diff --git a/dist/angular-patternfly.js b/dist/angular-patternfly.js
new file mode 100644
index 000000000..0f4128282
--- /dev/null
+++ b/dist/angular-patternfly.js
@@ -0,0 +1,10650 @@
+/**
+ * @name patternfly card
+ *
+ * @description
+ * Card module for patternfly.
+ *
+ */
+angular.module('patternfly.card', []);
+;/**
+ * @name patternfly
+ *
+ * @description
+ * Charts module for patternfly. Must Include d3.js and c3.js to use
+ *
+ */
+angular.module('patternfly.charts', ['patternfly.utils', 'ui.bootstrap', 'ngSanitize']);
+
+;/**
+ * @name patternfly card
+ *
+ * @description
+ * Filters module for patternfly.
+ *
+ */
+angular.module('patternfly.filters', ['patternfly.select', 'ui.bootstrap']);
+;/**
+ * @name patternfly.form
+ *
+ * @description
+ * Module for formting related functionality, primarily filters.
+ */
+angular.module('patternfly.form', []);
+;/**
+ * @name patternfly
+ *
+ * @description
+ * Modal module for patternfly.
+ *
+ */
+angular.module('patternfly.modals', ['ui.bootstrap.modal', 'ui.bootstrap.tpls']);
+;/**
+ * @name patternfly navigation
+ *
+ * @description
+ * Navigation module for patternfly.
+ *
+ */
+angular.module('patternfly.navigation', ['ui.bootstrap']);
+;/**
+ * @name patternfly notification
+ *
+ * @description
+ * Notification module for patternfly.
+ *
+ */
+angular.module('patternfly.notification', ['patternfly.utils']);
+;/**
+ * @name patternfly
+ *
+ * @description
+ * Base module for patternfly.
+ */
+angular.module('patternfly', [
+ 'patternfly.autofocus',
+ 'patternfly.card',
+ 'patternfly.filters',
+ 'patternfly.form',
+ 'patternfly.modals',
+ 'patternfly.navigation',
+ 'patternfly.notification',
+ 'patternfly.select',
+ 'patternfly.sort',
+ 'patternfly.toolbars',
+ 'patternfly.utils',
+ 'patternfly.validation',
+ 'patternfly.views',
+ 'patternfly.wizard'
+]);
+
+;/**
+ * @name patternfly card
+ *
+ * @description
+ * Sort module for patternfly.
+ *
+ */
+angular.module('patternfly.sort', ['ui.bootstrap']);
+;/**
+ * @name patternfly toolbars
+ *
+ * @description
+ * Filters module for patternfly.
+ *
+ */
+angular.module('patternfly.toolbars', [
+ 'patternfly.utils',
+ 'patternfly.filters',
+ 'patternfly.sort',
+ 'patternfly.views']);
+;
+angular.module( 'patternfly.utils', ['ui.bootstrap'] );
+;/**
+ * @name patternfly
+ *
+ * @description
+ * Views module for patternfly.
+ *
+ */
+angular.module('patternfly.views', ['patternfly.utils', 'patternfly.filters', 'patternfly.sort', 'patternfly.charts', 'dndLists']);
+;/**
+ * @name PatternFly Wizard
+ *
+ * @description
+ * Wizard module.
+ *
+ */
+angular.module('patternfly.wizard', ['ui.bootstrap.modal',
+ 'ui.bootstrap.tpls',
+ 'patternfly.form']);
+
+
+;/**
+ * @ngdoc directive
+ * @name patternfly.autofocus:pfFocused
+ * @restrict A
+ * @element ANY
+ * @param {expression=} pfFocused If the expression is true, the element is focused and selected (if possible).
+ *
+ * @description
+ * The focus on element is evaluated from given expression. If the expression provided as an attribute to this directive
+ * is evaluated as true, the element is selected (and focused).
+ *
+ * @example
+
+
+
+
+
+
+
+
+
+ */
+
+angular.module('patternfly.autofocus', []).directive('pfFocused', ["$timeout", function ($timeout) {
+ 'use strict';
+
+ return {
+ restrict: 'A',
+ link: function (scope, element, attrs) {
+ scope.$watch(attrs.pfFocused, function (newValue) {
+ $timeout(function () {
+ if (newValue) {
+ element[0].focus();
+ if (element[0].select) {
+ element[0].select();
+ }
+ }
+ });
+ });
+ }
+ };
+}]);
+;/**
+ * @ngdoc directive
+ * @name patternfly.card.directive:pfAggregateStatusCard
+ * @restrict A
+ * @element ANY
+ * @param {object} status Status configuration information
+ *
+ *
.title - the main title of the aggregate status card
+ *
.count - the number count of the main statuses
+ *
.href - the href to navigate to if one clicks on the title or count
+ *
.iconClass - an icon to display to the left of the count
+ *
.iconImage - an image to display to the left of the count
+ *
.notifications - an array of status icons & counts
+ *
+ *
.iconClass - an icon to display to the right of the notification count
+ *
.iconImage - an image to display to the left of the notification count
+ *
.count - the number count of the notification status
+ *
.href - href to navigate to if one clicks on the notification status icon or count
+ *
+ *
+ * When layout='mini', only one notification can be specified:
+ *
+ *
...
+ *
.notification - an object of containing a single notification icon & count
+ *
+ *
.iconClass - an icon to display to the right of the notification count
+ *
.iconImage - an image to display to the left of the notification count
+ *
.count - the number count of the notification status
+ *
.href - href to navigate to if one clicks on the notification status icon or count
+ *
+ *
+ * @param {boolean=} show-top-border Show/hide the top border, true shows top border, false (default) hides top border
+ * @param {string=} layout Various alternative layouts the aggregate status card may have:
+ *
+ *
'mini' displays a mini aggregate status card. Note: when using 'mini' layout, only one notification can be specified in the status object
+ *
'tall' displays a tall aggregate status card. This equals the depreciated 'alt-layout' param.
+ *
+ * @deprecated {boolean=} alt-layout Display the aggregate status card in a 'alternate tall' layout. false (default) displays normal layout, true displays tall layout
+ *
+ * @description
+ * Directive for easily displaying status information
+ *
+ * @example
+
+
+
+
.iconClass - (optional) the icon to show on the bottom left of the footer panel
+ *
.text - (optional) the text to show on the bottom left of the footer panel, to the right of the icon
+ *
.href - (optional) the href link to navigate to when the footer href is clicked
+ *
.callBackFn - (optional) user defined function to call when the footer href is clicked
+ *
+ * *Note: If a href link and a callBackFn are specified, the href link will be called
+ * @param {object=} filter filter configuration properties:
+ *
.iconClass - (optional) the icon to show on the bottom left of the footer panel
+ *
.text - (optional) the text to show on the bottom left of the footer panel, to the right of the icon
+ *
.href - (optional) the href link to navigate to when the footer href is clicked
+ *
.callBackFn - (optional) user defined function to call when the footer href is clicked
+ *
+ * *Note: If a href link and a callBackFn are specified, the href link will be called
+ * @param {object=} filter filter configuration properties:
+ *
.defaultFilter - integer, 0 based index into the filters array
+ *
.callBackFn - user defined function to call when a filter is selected
+ *
+ * @description
+ * Directive for easily displaying a card with html content
+ *
+ * @example
+
+
+
+
+
+
+ Card Contents
+
+
+
+ Card Contents
+
+
+
+
+ angular.module( 'demo', ['patternfly.charts', 'patternfly.card'] ).controller( 'ChartCtrl', function( $scope ) {
+
+ $scope.footerConfig = {
+ 'iconClass' : 'fa fa-flag',
+ 'text' : 'View All Events',
+ 'callBackFn': function () {
+ alert("Footer Callback Fn Called");
+ }
+ }
+
+ $scope.filterConfigHeader = {
+ 'filters' : [{label:'Last 30 Days', value:'30'},
+ {label:'Last 15 Days', value:'15'},
+ {label:'Today', value:'today'}],
+ 'callBackFn': function (f) {
+ alert("Header Filter Callback Fn Called for '" + f.label + "' value = " + f.value);
+ },
+ 'position' : 'header'
+ }
+
+ $scope.filterConfig = {
+ 'filters' : [{label:'Last 30 Days', value:'30'},
+ {label:'Last 15 Days', value:'15'},
+ {label:'Today', value:'today'}],
+ 'callBackFn': function (f) {
+ alert("Filter Callback Fn Called for '" + f.label + "' value = " + f.value);
+ },
+ 'defaultFilter' : '1'
+ }
+ });
+
+
+ */
+;/**
+ * @ngdoc directive
+ * @name patternfly.card.directive:pfCard - Trends
+ * @restrict A
+ * @element ANY
+ * @param {string} headTitle Title for the card
+ * @param {string=} subTitle Sub-Title for the card
+ * @param {boolean=} showTopBorder Show/Hide the blue top border. True shows top border, false (default) hides top border
+ * @param {boolean=} showTitlesSeparator Show/Hide the grey line between the title and sub-title.
+ * True (default) shows the line, false hides the line
+ * @param {object=} footer footer configuration properties:
+ *
+ *
.iconClass - (optional) the icon to show on the bottom left of the footer panel
+ *
.text - (optional) the text to show on the bottom left of the footer panel, to the right of the icon
+ *
.href - (optional) the href link to navigate to when the footer href is clicked
+ *
.callBackFn - (optional) user defined function to call when the footer href is clicked
+ *
+ * *Note: If a href link and a callBackFn are specified, the href link will be called
+ * @param {object=} filter filter configuration properties:
+ *
+
+
+
+ angular.module( 'patternfly.charts' ).controller( 'ChartCtrl', function( $scope ) {
+ $scope.used = 950;
+ $scope.total = 1000;
+ $scope.available = $scope.total - $scope.used;
+
+ $scope.chartConfig = patternfly.c3ChartDefaults().getDefaultDonutConfig('MHz Used');
+ $scope.chartConfig.data = {
+ type: "donut",
+ columns: [
+ ["Used", $scope.used],
+ ["Available", $scope.total - $scope.used]
+ ],
+ groups: [
+ ["used", "available"]
+ ],
+ order: null
+ };
+
+ $scope.getChart = function (chart) {
+ $scope.chart = chart;
+ }
+
+ $scope.focusUsed = function () {
+ $scope.chart.focus("Used");
+ }
+
+ $scope.updateAvailable = function (val) {
+ $scope.available = $scope.total - $scope.used;
+ }
+
+ $scope.submitform = function (val) {
+ $scope.used = val;
+ $scope.updateAvailable();
+ $scope.chartConfig.data.columns = [["Used",$scope.used],["Available",$scope.available]];
+ };
+ });
+
+
+ */
+(function (patternfly) {
+ 'use strict';
+
+ angular.module('patternfly.charts').directive('pfC3Chart', ["$timeout", function ($timeout) {
+ return {
+ restrict: 'A',
+ scope: {
+ config: '=',
+ getChartCallback: '='
+ },
+ template: '',
+ replace: true,
+ link: function (scope, element, attrs) {
+ // store the chart object
+ var chart = undefined;
+ scope.$watch('config', function () {
+ $timeout(function () {
+ //generate c3 chart data
+ var chartData = scope.config;
+ if (chartData) {
+ chartData.bindto = '#' + attrs.id;
+ //checks if the chart is created
+ if (!chart) {
+ chart = c3.generate(chartData);
+ }else {
+ //if it is created, then, we only need to load changes
+ chart.load(scope.config.data);
+ }
+ if (scope.getChartCallback) {
+ scope.getChartCallback(chart);
+ }
+ }
+ });
+ }, true);
+ }
+ };
+ }]);
+}(patternfly));
+;/**
+ * @ngdoc directive
+ * @name patternfly.charts.directive:pfDonutPctChart
+ *
+ * @description
+ * Directive for rendering a percentage used donut/radial chart. The Used Percentage fill starts at 12 o’clock and
+ * moves clockwise. Whatever portion of the donut not Used, will be represented as Available, and rendered as a
+ * gray fill.
+ * There are three possible fill colors for Used Percentage, dependent on whether or not there are thresholds:
+ *
+ *
When no thresholds exist, or if the used percentage has not surpassed any thresholds, the indicator is blue.
+ *
When the used percentage has surpassed the warning threshold, but not the error threshold, the indicator is orange.
+ *
When the used percentage has surpassed the error threshold, the indicator is is red.
+ *
+ * The directive will calculate the Available Percentage (Total - Used), and display it as a grey radial fill.
+ *
+ *
+ * See http://c3js.org/reference.html for a full list of C3 chart options.
+ *
+ * @param {object} config configuration properties for the donut chart:
+ *
+ *
.chartId - the unique id of the donut chart
+ *
.units - unit label for values, ex: 'MHz','GB', etc..
+ *
.thresholds - warning and error percentage thresholds used to determine the Usage Percentage fill color (optional)
+ *
.tooltipFn(d) - user defined function to customize the tool tip (optional)
+ *
.centerLabelFn - user defined function to customize the text of the center label (optional)
+ *
.onClickFn(d,i) - user defined function to handle when donut arc is clicked upon.
+ *
+ *
+ * @param {object} data the Total and Used values for the donut chart. Available is calculated as Total - Used.
+ *
+ *
.used - number representing the amount used
+ *
.total - number representing the total amount
+ *
.dataAvailable - Flag if there is data available - default: true
+ *
+ *
+ * @param {string=} center-label specifies the contents of the donut's center label.
+ * Values:
+ *
+ *
'used' - displays the Used amount in the center label (default)
+ *
'available' - displays the Available amount in the center label
+ *
'percent' - displays the Usage Percent of the Total amount in the center label
+ *
'none' - does not display the center label
+ *
+ *
+ * @param {int=} chartHeight height of the donut chart
+
+ * @example
+
+
+
+ *
+ * @param {boolean=} chartDataAvailable flag if the chart data is available - default: true
+ * @param {number=} height height of the chart (no units) - default: 200
+ * @param {string=} chartTitle title of the chart
+ * @param {boolean=} showLegend flag to show the legend, defaults to true
+ * @param {array=} legendLabels the labels for the legend - defaults: ['< 70%', '70-80%' ,'80-90%', '> 90%']
+ * @param {number=} maxBlockSize the maximum size for blocks in the heatmap. Default: 50, Range: 5 - 50
+ * @param {number=} minBlockSize the minimum size for blocks in the heatmap. Default: 2
+ * @param {number=} blockPadding the padding in pixels between blocks (default: 2)
+ * @param {array=} thresholds the threshold values for the heapmap - defaults: [0.7, 0.8, 0.9]
+ * @param {array=} heatmapColorPattern the colors that correspond to the various threshold values (lowest to hightest value ex: <70& to >90%) - defaults: ['#d4f0fa', '#F9D67A', '#EC7A08', '#CE0000']
+ * @param {function=} clickAction function(block) function to call when a block is clicked on
+ * @param {number=} rangeHoverSize the maximum size for highlighting blocks in the same range. Default: 15
+ * @param {boolean=} rangeOnHover flag to highlight blocks in the same range on hover, defaults to true
+ * @param {array=} rangeTooltips the tooltips for blocks in the same range - defaults: ['< 70%', '70-80%' ,'80-90%', '> 90%']
+ * @example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ angular.module( 'patternfly.charts' ).controller( 'ChartCtrl', function( $scope) {
+ $scope.data = [
+ {'id': 9,'value': 0.96,'tooltip': 'Node 8 : My OpenShift Provider 96% : 96 Used of 100 Total 4 Available'},
+ {'id': 44, 'value': 0.94, 'tooltip': 'Node 19 : My Kubernetes Provider 94% : 94 Used of 100 Total 6 Available'},
+ {'id': 0, 'value': 0.91, 'tooltip': 'Node 9 : My OpenShift Provider 91% : 91 Used of 100 Total 9 Available'},
+ {'id': 43, 'value': 0.9, 'tooltip': 'Node 18 : My Kubernetes Provider 90% : 90 Used of 100 Total 10 Available'},
+ {'id': 7, 'value': 0.89, 'tooltip': 'Node 12 : My OpenShift Provider 89% : 89 Used of 100 Total 11 Available'},
+ {'id': 41, 'value': 0.82, 'tooltip': 'Node 16 : My Kubernetes Provider 82% : 82 Used of 100 Total 18 Available'},
+ {'id': 21, 'value': 0.81, 'tooltip': 'Node 21 : My OpenShift Provider 81% : 81 Used of 100 Total 19 Available'},
+ {'id': 26, 'value': 0.8, 'tooltip': 'Node 1 : My Kubernetes Provider 80% : 80 Used of 100 Total 20 Available'},
+ {'id': 48, 'value': 0.74, 'tooltip': 'Node 23 : My Kubernetes Provider 74% : 74 Used of 100 Total 26 Available'},
+ {'id': 27, 'value': 0.72, 'tooltip': 'Node 2 : My Kubernetes Provider 72% : 72 Used of 100 Total 28 Available'},
+ {'id': 42, 'value': 0.71, 'tooltip': 'Node 17 : My Kubernetes Provider 71% : 71 Used of 100 Total 29 Available'},
+ {'id': 23, 'value': 0.71, 'tooltip': 'Node 23 : My OpenShift Provider 71% : 71 Used of 100 Total 29 Available'},
+ {'id': 22, 'value': 0.69, 'tooltip': 'Node 22 : My OpenShift Provider 69% : 69 Used of 100 Total 31 Available'},
+ {'id': 2, 'value': 0.66, 'tooltip': 'Node 2 : M8y OpenShift Provider 66% : 66 Used of 100 Total 34 Available'},
+ {'id': 39, 'value': 0.66, 'tooltip': 'Node 14 : My Kubernetes Provider 66% : 66 Used of 100 Total 34 Available'},
+ {'id': 3, 'value': 0.65, 'tooltip': 'Node 39 : My OpenShift Provider 65% : 65 Used of 100 Total 35 Available'},
+ {'id': 29, 'value': 0.65, 'tooltip': 'Node 4 : My Kubernetes Provider 65% : 65 Used of 100 Total 35 Available'},
+ {'id': 32, 'value': 0.56, 'tooltip': 'Node 7 : My Kubernetes Provider 56% : 56 Used of 100 Total 44 Available'},
+ {'id': 13, 'value': 0.56, 'tooltip': 'Node 13 : My OpenShift Provider 56% : 56 Used of 100 Total 44 Available'},
+ {'id': 49, 'value': 0.52, 'tooltip': 'Node 24 : My Kubernetes Provider 52% : 52 Used of 100 Total 48 Available'},
+ {'id': 36, 'value': 0.5, 'tooltip': 'Node 11 : My Kubernetes Provider 50% : 50 Used of 100 Total 50 Available'},
+ {'id': 6, 'value': 0.5, 'tooltip': 'Node 5 : My OpenShift Provider 50% : 50 Used of 100 Total 50 Available'},
+ {'id': 38, 'value': 0.49, 'tooltip': 'Node 13 : My Kubernetes Provider 49% : 49 Used of 100 Total 51 Available'},
+ {'id': 15, 'value': 0.48, 'tooltip': 'Node 15 : My OpenShift Provider 48% : 48 Used of 100 Total 52 Available'},
+ {'id': 30, 'value': 0.48, 'tooltip': 'Node 5 : My Kubernetes Provider 48% : 48 Used of 100 Total 52 Available'},
+ {'id': 11, 'value': 0.47, 'tooltip': 'Node 11 : My OpenShift Provider 47% : 47 Used of 100 Total 53 Available'},
+ {'id': 17, 'value': 0.46, 'tooltip': 'Node 17 : My OpenShift Provider 46% : 46 Used of 100 Total 54 Available'},
+ {'id': 25, 'value': 0.45, 'tooltip': 'Node 0 : My Kubernetes Provider 45% : 45 Used of 100 Total 55 Available'},
+ {'id': 50, 'value': 0.45, 'tooltip': 'Node 25 : My Kubernetes Provider 45% : 45 Used of 100 Total 55 Available'},
+ {'id': 46, 'value': 0.45, 'tooltip': 'Node 21 : My Kubernetes Provider 45% : 45 Used of 100 Total 55 Available'},
+ {'id': 47, 'value': 0.45, 'tooltip': 'Node 22 : My Kubernetes Provider 45% : 45 Used of 100 Total 55 Available'},
+ {'id': 1, 'value': 0.44, 'tooltip': 'Node 1 : My OpenShift Provider 44% : 44 Used of 100 Total 56 Available'},
+ {'id': 31, 'value': 0.44, 'tooltip': 'Node 6 : My Kubernetes Provider 44% : 44 Used of 100 Total 56 Available'},
+ {'id': 37, 'value': 0.44, 'tooltip': 'Node 12 : My Kubernetes Provider 44% : 44 Used of 100 Total 56 Available'},
+ {'id': 24, 'value': 0.44, 'tooltip': 'Node 24 : My OpenShift Provider 44% : 44 Used of 100 Total 56 Available'},
+ {'id': 40, 'value': 0.43, 'tooltip': 'Node 40 : My Kubernetes Provider 43% : 43 Used of 100 Total 57 Available'},
+ {'id': 20, 'value': 0.39, 'tooltip': 'Node 20 : My OpenShift Provider 39% : 39 Used of 100 Total 61 Available'},
+ {'id': 8, 'value': 0.39, 'tooltip': 'Node 8 : My OpenShift Provider 39% : 39 Used of 100 Total 61 Available'},
+ {'id': 5, 'value': 0.38, 'tooltip': 'Node 5 : My OpenShift Provider 38% : 38 Used of 100 Total 62 Available'},
+ {'id': 45, 'value': 0.37, 'tooltip': 'Node 20 : My Kubernetes Provider 37% : 37 Used of 100 Total 63 Available'},
+ {'id': 12, 'value': 0.37, 'tooltip': 'Node 12 : My OpenShift Provider 37% : 37 Used of 100 Total 63 Available'},
+ {'id': 34, 'value': 0.37, 'tooltip': 'Node 9 : My Kubernetes Provider 37% : 37 Used of 100 Total 63 Available'},
+ {'id': 33, 'value': 0.33, 'tooltip': 'Node 8 : My Kubernetes Provider 33% : 33 Used of 100 Total 67 Available'},
+ {'id': 16, 'value': 0.32, 'tooltip': 'Node 16 : My OpenShift Provider 32% : 32 Used of 100 Total 68 Available'},
+ {'id': 10, 'value': 0.29, 'tooltip': 'Node 10 : My OpenShift Provider 28% : 29 Used of 100 Total 71 Available'},
+ {'id': 35, 'value': 0.28, 'tooltip': 'Node 35 : My Kubernetes Provider 28% : 28 Used of 100 Total 72 Available'},
+ {'id': 18, 'value': 0.27, 'tooltip': 'Node 18 : My OpenShift Provider 27% : 27 Used of 100 Total 73 Available'},
+ {'id': 4, 'value': 0.26, 'tooltip': 'Node 4 : My OpenShift Provider 26% : 26 Used of 100 Total 74 Available'},
+ {'id': 19, 'value': 0.25, 'tooltip': 'Node 19 : My OpenShift Provider 25% : 25 Used of 100 Total 75 Available'},
+ {'id': 28, 'value': 0.25, 'tooltip': 'Node 3 : My Kubernetes Provider 25% : 25 Used of 100 Total 75 Available'},
+ {'id': 51, 'value': 0.22, 'tooltip': 'Node 26 : My Kubernetes Provider 22% : 22 Used of 100 Total 78 Available'},
+ {'id': 14, 'value': 0.2, 'tooltip': 'Node 14 : My OpenShift Provider 20% : 20 Used of 100 Total 80 Available'}];
+
+ $scope.dataAvailable = true;
+ $scope.title = 'Utilization - Using Defaults';
+ $scope.titleAlt = 'Utilization - Overriding Defaults';
+ $scope.titleSmall = 'Utilization - Small Blocks';
+ $scope.legendLabels = ['< 60%','70%', '70-80%' ,'80-90%', '> 90%'];
+ $scope.rangeTooltips = ['Memory Utilization < 70% 40 Nodes', 'Memory Utilization 70-80% 4 Nodes', 'Memory Utilization 80-90% 4 Nodes', 'Memory Utilization > 90% 4 Nodes'];
+ $scope.thresholds = [0.6, 0.7, 0.8, 0.9];
+ $scope.heatmapColorPattern = ['#d4f0fa', '#F9D67A', '#EC7A08', '#CE0000', '#f00'];
+
+ $scope.showLegends = true;
+ var clickAction = function (block) {
+ console.log(block);
+ };
+ $scope.clickAction = clickAction;
+ });
+
+
+ */
+angular.module('patternfly.charts').directive('pfHeatmap', ["$compile", "$window", function ($compile, $window) {
+ 'use strict';
+ return {
+ restrict: 'A',
+ scope: {
+ data: '=',
+ chartDataAvailable: '=?',
+ height: '=?',
+ chartTitle: '=?',
+ showLegend: '=?',
+ legendLabels: '=?',
+ maxBlockSize: '@',
+ minBlockSize: '@',
+ blockPadding: '@',
+ thresholds: '=?',
+ heatmapColorPattern: '=?',
+ clickAction: '=?',
+ rangeOnHover: '=?',
+ rangeHoverSize: '@',
+ rangeTooltips: '=?'
+ },
+ templateUrl: 'charts/heatmap/heatmap.html',
+ controller: ["$scope", function ($scope) {
+ var thresholdDefaults = [0.7, 0.8, 0.9];
+ var heatmapColorPatternDefaults = ['#d4f0fa', '#F9D67A', '#EC7A08', '#CE0000'];
+ var legendLabelDefaults = ['< 70%', '70-80%' ,'80-90%', '> 90%'];
+ var rangeTooltipDefaults = ['< 70%', '70-80%' ,'80-90%', '> 90%'];
+ var heightDefault = 200;
+
+ //Allow overriding of defaults
+ if ($scope.maxBlockSize === undefined || isNaN($scope.maxBlockSize)) {
+ $scope.maxSize = 64;
+ } else {
+ $scope.maxSize = parseInt($scope.maxBlockSize);
+ if ($scope.maxSize < 5) {
+ $scope.maxSize = 5;
+ } else if ($scope.maxSize > 50) {
+ $scope.maxSize = 50;
+ }
+ }
+
+ if ($scope.minBlockSize === undefined || isNaN($scope.minBlockSize)) {
+ $scope.minSize = 2;
+ } else {
+ $scope.minSize = parseInt($scope.minBlockSize);
+ }
+
+ if ($scope.blockPadding === undefined || isNaN($scope.blockPadding)) {
+ $scope.padding = 2;
+ } else {
+ $scope.padding = parseInt($scope.blockPadding);
+ }
+
+ if ($scope.rangeHoverSize === undefined || isNaN($scope.rangeHoverSize)) {
+ $scope.rangeHoverSize = 15;
+ } else {
+ $scope.rangeHoverSize = parseInt($scope.rangeHoverSize);
+ }
+
+ $scope.rangeOnHover = ($scope.rangeOnHover === undefined || $scope.rangeOnHover) ? true : false;
+
+ if (!$scope.rangeTooltips) {
+ $scope.rangeTooltips = rangeTooltipDefaults;
+ }
+
+ if (!$scope.thresholds) {
+ $scope.thresholds = thresholdDefaults;
+ }
+
+ if (!$scope.heatmapColorPattern) {
+ $scope.heatmapColorPattern = heatmapColorPatternDefaults;
+ }
+
+ if (!$scope.legendLabels) {
+ $scope.legendLabels = legendLabelDefaults;
+ }
+ $scope.height = $scope.height || heightDefault;
+ $scope.showLegend = $scope.showLegend || ($scope.showLegend === undefined);
+ $scope.loadingDone = false;
+ }],
+ link: function (scope, element, attrs) {
+ var thisComponent = element[0].querySelector('.heatmap-pf-svg');
+ var containerWidth, containerHeight, blockSize, numberOfRows;
+
+ var setStyles = function () {
+ scope.containerStyles = {
+ height: scope.height + 'px',
+ display: scope.chartDataAvailable === false ? 'none' : 'block'
+ };
+ };
+
+ var setSizes = function () {
+ var parentContainer = element[0].querySelector('.heatmap-container');
+ containerWidth = parentContainer.clientWidth;
+ containerHeight = parentContainer.clientHeight;
+ blockSize = determineBlockSize();
+
+ if ((blockSize - scope.padding) > scope.maxSize) {
+ blockSize = scope.padding + scope.maxSize;
+
+ // Attempt to square off the area, check if square fits
+ numberOfRows = Math.ceil(Math.sqrt(scope.data.length));
+ if (blockSize * numberOfRows > containerWidth ||
+ blockSize * numberOfRows > containerHeight) {
+ numberOfRows = (blockSize === 0) ? 0 : Math.floor(containerHeight / blockSize);
+ }
+ } else if ((blockSize - scope.padding) < scope.minSize) {
+ blockSize = scope.padding + scope.minSize;
+
+ // Attempt to square off the area, check if square fits
+ numberOfRows = Math.ceil(Math.sqrt(scope.data.length));
+ if (blockSize * numberOfRows > containerWidth ||
+ blockSize * numberOfRows > containerHeight) {
+ numberOfRows = (blockSize === 0) ? 0 : Math.floor(containerHeight / blockSize);
+ }
+ } else {
+ numberOfRows = (blockSize === 0) ? 0 : Math.floor(containerHeight / blockSize);
+ }
+ };
+
+ var determineBlockSize = function () {
+ var x = containerWidth;
+ var y = containerHeight;
+ var n = scope.data ? scope.data.length : 0;
+ var px = Math.ceil(Math.sqrt(n * x / y));
+ var py = Math.ceil(Math.sqrt(n * y / x));
+ var sx, sy;
+
+ if (Math.floor(px * y / x) * px < n) {
+ sx = y / Math.ceil(px * y / x);
+ } else {
+ sx = x / px;
+ }
+
+ if (Math.floor(py * x / y) * py < n) {
+ sy = x / Math.ceil(x * py / y);
+ } else {
+ sy = y / py;
+ }
+ return Math.max(sx, sy);
+ };
+
+ var redraw = function () {
+ var data = scope.data;
+ var color = d3.scale.threshold().domain(scope.thresholds).range(scope.heatmapColorPattern);
+ var rangeTooltip = d3.scale.threshold().domain(scope.thresholds).range(scope.rangeTooltips);
+ var blocks;
+ var fillSize = blockSize - scope.padding;
+ var highlightBlock = function (block, active) {
+ block.style('fill-opacity', active ? 1 : 0.4);
+ };
+ var highlightBlockColor = function (block, fillColor) {
+ // Get fill color from given block
+ var blockColor = color(block.map(function (d) {
+ return d[0].__data__.value;
+ }));
+ // If given color matches, apply highlight
+ if (blockColor === fillColor) {
+ block.style('fill-opacity', 1);
+ }
+ };
+
+ var svg = window.d3.select(thisComponent);
+ svg.selectAll('*').remove();
+ blocks = svg.selectAll('rect').data(data).enter().append('rect');
+ blocks.attr('x', function (d, i) {
+ return Math.floor(i / numberOfRows) * blockSize;
+ }).attr('y', function (d, i) {
+ return i % numberOfRows * blockSize;
+ }).attr('width', fillSize).attr('height', fillSize).style('fill', function (d) {
+ return color(d.value);
+ }).attr('uib-tooltip-html', function (d, i) { //tooltip-html is throwing an exception
+ if (scope.rangeOnHover && fillSize <= scope.rangeHoverSize) {
+ return '"' + rangeTooltip(d.value) + '"';
+ }
+ return "'" + d.tooltip + "'";
+ }).attr('tooltip-append-to-body', function (d, i) {
+ return true;
+ }).attr('tooltip-animation', function (d, i) {
+ return false;
+ });
+
+ //Adding events
+ blocks.on('mouseover', function () {
+ var fillColor;
+ blocks.call(highlightBlock, false);
+ if (scope.rangeOnHover && fillSize <= scope.rangeHoverSize) {
+ // Get fill color for current block
+ fillColor = color(d3.select(this).map(function (d) {
+ return d[0].__data__.value;
+ }));
+ // Highlight all blocks matching fill color
+ blocks[0].forEach(function (block) {
+ highlightBlockColor(d3.select(block), fillColor);
+ });
+ } else {
+ d3.select(this).call(highlightBlock, true);
+ }
+ });
+ blocks.on('click', function (d) {
+ if (scope.clickAction) {
+ scope.clickAction(d);
+ }
+ });
+
+ //Compiles the tooltips
+ angular.forEach(angular.element(blocks), function (block) {
+ var el = angular.element(block);
+ $compile(el)(scope);
+ });
+
+ svg.on('mouseleave', function () {
+ blocks.call(highlightBlock, true);
+ });
+ };
+
+ scope.$watch('data', function (newVal, oldVal) {
+ if (typeof(newVal) !== 'undefined') {
+ scope.loadingDone = true;
+ setStyles();
+ if (scope.chartDataAvailable !== false) {
+ setSizes();
+ redraw();
+ }
+ }
+ });
+ scope.$watch('chartDataAvailable', function () {
+ if (scope.chartDataAvailable === false) {
+ scope.loadingDone = true;
+ }
+ setStyles();
+ });
+
+ angular.element($window).on('resize', function () {
+ setSizes();
+ redraw();
+ });
+
+ scope.$watch(
+ function () {
+ return [element[0].offsetWidth, element[0].offsetHeight].join('x');
+ },
+ function (value) {
+ setSizes();
+ redraw();
+ }
+ );
+ }
+ };
+}]);
+;/**
+ * @ngdoc directive
+ * @name patternfly.charts.directive:pfLineChart
+ *
+ * @description
+ * Directive for rendering a line chart.
+ *
+ * See http://c3js.org/reference.html for a full list of C3 chart options.
+ *
+ * @param {object} config configuration settings for the line chart:
+ *
+ *
.chartId - the ID of the container that the chart should bind to
+ *
.units - unit label for values, ex: 'MHz','GB', etc..
+ *
.tooltipFn - (optional) override the tooltip contents generation functions. Should take a data point and
+ * return HTML markup for the tooltip contents. Setting this overrides the tooltipType value.
+ *
.area - (optional) overrides the default Area properties of the C3 chart
+ *
.size - (optional) overrides the default Size properties of the C3 chart
+ *
.axis - (optional) overrides the default Axis properties of the C3 chart
+ *
.color - (optional) overrides the default Color properties of the C3 chart
+ *
.legend - (optional) overrides the default Legend properties of the C3 chart
+ *
.point - (optional) overrides the default Point properties of the C3 chart
+ *
+ *
+ * @param {object} chartData the data to be shown as an area chart
+ * First and second Array elements, xData and yData, must exist, next data arrays are optional.
+ *
+ *
.xData - Array, X values for the data points, first element must be the name of the data
+ *
.yData - Array, Y Values for the data points, first element must be the name of the data
+ *
.yData1 - Array, Y Values for the data points, first element must be the name of the data
+ *
.[...] - Array, Y Values for the data points, first element must be the name of the data
+ *
+ *
+ * @param {boolean=} showXAxis override config settings for showing the X Axis
+ * @param {boolean=} showYAxis override config settings for showing the Y Axis
+ * @param {boolean=} setAreaChart override config settings for showing area type chart
+
+ * @example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ angular.module( 'patternfly.charts' ).controller( 'ChartCtrl', function( $scope, pfUtils ) {
+
+ $scope.config = {
+ chartId: 'exampleLine',
+ grid: {y: {show: false}},
+ point: {r: 1},
+ color: {pattern: [pfUtils.colorPalette.blue, pfUtils.colorPalette.green]}
+ };
+
+ var today = new Date();
+ var dates = ['dates'];
+ for (var d = 20 - 1; d >= 0; d--) {
+ dates.push(new Date(today.getTime() - (d * 24 * 60 * 60 * 1000)));
+ }
+
+ $scope.data = {
+ dataAvailable: true,
+ xData: dates,
+ yData0: ['Created', 12, 10, 10, 62, 17, 10, 15, 13, 17, 10, 12, 10, 10, 12, 17, 16, 15, 13, 17, 10],
+ yData1: ['Deleted', 10, 17, 76, 14, 10, 10, 10, 10, 10, 10, 10, 17, 17, 14, 10, 10, 10, 10, 10, 10]
+ };
+
+ $scope.custShowXAxis = false;
+ $scope.custShowYAxis = false;
+ $scope.custAreaChart = false;
+
+ $scope.addDataPoint = function () {
+ $scope.data.xData.push(new Date($scope.data.xData[$scope.data.xData.length - 1].getTime() + (24 * 60 * 60 * 1000)));
+ $scope.data.yData0.push(Math.round(Math.random() * 100));
+ $scope.data.yData1.push(Math.round(Math.random() * 100));
+ };
+
+ $scope.resetData = function () {
+ $scope.data = {
+ xData: dates,
+ yData0: ['Created', 12, 10, 10, 62],
+ yData1: ['Deleted', 10, 17, 76, 14]
+ };
+ };
+ });
+
+
+ */
+(function (patternfly) {
+ 'use strict';
+ angular.module('patternfly.charts').directive('pfLineChart', ["pfUtils", function (pfUtils) {
+ return {
+ restrict: 'A',
+ scope: {
+ config: '=',
+ chartData: '=',
+ showXAxis: '=?',
+ showYAxis: '=?',
+ setAreaChart: '=?'
+ },
+ replace: true,
+ templateUrl: 'charts/line/line-chart.html',
+ controller: ['$scope',
+ function ($scope) {
+
+ // Create an ID for the chart based on the chartId in the config if given
+ $scope.lineChartId = 'lineChart';
+ if ($scope.config.chartId) {
+ $scope.lineChartId = $scope.config.chartId + $scope.lineChartId;
+ }
+
+ /*
+ * Convert the config data to C3 Data
+ */
+ $scope.getLineData = function (chartData) {
+ var lineData = {
+ type: $scope.setAreaChart ? "area" : "line"
+ };
+
+ if (chartData && chartData.dataAvailable !== false && chartData.xData) {
+ lineData.x = chartData.xData[0];
+ // Convert the chartData dictionary into a C3 columns data arrays
+ lineData.columns = Object.keys (chartData).map (function (key) {
+ return chartData[key];
+ });
+ }
+
+ return lineData;
+ };
+
+ /*
+ * Setup Axis options. Default is to not show either axis. This can be overridden in two ways:
+ * 1) in the config, setting showAxis to true will show both axes
+ * 2) in the attributes showXAxis and showYAxis will override the config if set
+ *
+ * By default only line and the tick marks are shown, no labels. This is a line and should be used
+ * only to show a brief idea of trending. This can be overridden by setting the config.axis options per C3
+ */
+
+ if ($scope.showXAxis === undefined) {
+ $scope.showXAxis = ($scope.config.showAxis !== undefined) && $scope.config.showAxis;
+ }
+
+ if ($scope.showYAxis === undefined) {
+ $scope.showYAxis = ($scope.config.showAxis !== undefined) && $scope.config.showAxis;
+ }
+
+ $scope.defaultConfig = patternfly.c3ChartDefaults().getDefaultLineConfig();
+ $scope.defaultConfig.axis = {
+ x: {
+ show: $scope.showXAxis === true,
+ type: 'timeseries',
+ tick: {
+ format: function () {
+ return '';
+ }
+ }
+ },
+ y: {
+ show: $scope.showYAxis === true,
+ tick: {
+ format: function () {
+ return '';
+ }
+ }
+ }
+ };
+
+ /*
+ * Setup Chart type option. Default is Line Chart.
+ */
+ if ($scope.setAreaChart === undefined) {
+ $scope.setAreaChart = ($scope.config.setAreaChart !== undefined) && $scope.config.setAreaChart;
+ }
+
+ // Convert the given data to C3 chart format
+ $scope.config.data = pfUtils.merge($scope.config.data, $scope.getLineData($scope.chartData));
+
+ // Override defaults with callers specifications
+ $scope.defaultConfig = pfUtils.merge($scope.defaultConfig, $scope.config);
+ }
+ ],
+
+ link: function (scope) {
+ scope.$watch('config', function () {
+ scope.config.data = pfUtils.merge(scope.config.data, scope.getLineData(scope.chartData));
+ scope.chartConfig = pfUtils.merge(scope.defaultConfig, scope.config);
+ }, true);
+ scope.$watch('showXAxis', function () {
+ scope.chartConfig.axis.x.show = scope.showXAxis === true;
+ });
+ scope.$watch('showYAxis', function () {
+ scope.chartConfig.axis.y.show = scope.showYAxis === true;
+ });
+ scope.$watch('setAreaChart', function () {
+ scope.chartConfig.data.type = scope.setAreaChart ? "area" : "line";
+ });
+ scope.$watch('chartData', function () {
+ scope.chartConfig.data = scope.getLineData(scope.chartData);
+ }, true);
+ }
+ };
+ }]);
+}(patternfly));
+
+;/**
+ * @ngdoc directive
+ * @name patternfly.charts.directive:pfSparklineChart
+ *
+ * @description
+ * Directive for rendering a sparkline chart.
+ *
+ * See http://c3js.org/reference.html for a full list of C3 chart options.
+ *
+ * @param {object} config configuration settings for the sparkline chart:
+ *
+ *
.chartId - the ID of the container that the chart should bind to
+ *
.units - unit label for values, ex: 'MHz','GB', etc..
+ *
.tooltipType - (optional) set the type of tooltip, valid values:
+ *
+ *
'default' - show the data point value and the data point name.
+ *
'usagePerDay' - show the date, percent used, and used value for the data point.
+ *
'valuePerDay' - show the date and value for the data point.
+ *
'percentage' - show the current data point as a percentage.
+ *
+ *
.tooltipFn - (optional) override the tooltip contents generation functions. Should take a data point and
+ * return HTML markup for the tooltip contents. Setting this overrides the tooltipType value.
+ *
.area - (optional) overrides the default Area properties of the C3 chart
+ *
.size - (optional) overrides the default Size properties of the C3 chart
+ *
.axis - (optional) overrides the default Axis properties of the C3 chart
+ *
.color - (optional) overrides the default Color properties of the C3 chart
+ *
.legend - (optional) overrides the default Legend properties of the C3 chart
+ *
.point - (optional) overrides the default Point properties of the C3 chart
+ *
+ *
+ * @param {object} chartData the data to be shown as an area chart
+ *
+ *
.xData - Array, X values for the data points, first element must be the name of the data
+ *
.yData - Array, Y Values for the data points, first element must be the name of the data
+ *
.total - (optional) The Total amount, used when determining percentages
+ *
.dataAvailable - Flag if there is data available - default: true
+ *
+ *
+ * @param {int=} chartHeight height of the sparkline chart
+ * @param {boolean=} showXAxis override config settings for showing the X Axis
+ * @param {boolean=} showYAxis override config settings for showing the Y Axis
+
+ * @example
+
+
+
';
+ break;
+ default:
+ tipRows = patternfly.c3ChartDefaults().getDefaultSparklineTooltip().contents(d);
+ }
+ }
+ return $scope.getTooltipTableHTML(tipRows);
+ },
+ position: function (data, width, height, element) {
+ var center;
+ var top;
+ var chartBox;
+ var graphOffsetX;
+ var x;
+
+ try {
+ center = parseInt(element.getAttribute('x'));
+ top = parseInt(element.getAttribute('y'));
+ chartBox = document.querySelector('#' + $scope.sparklineChartId).getBoundingClientRect();
+ graphOffsetX = document.querySelector('#' + $scope.sparklineChartId + ' g.c3-axis-y').getBoundingClientRect().right;
+ x = Math.max(0, center + graphOffsetX - chartBox.left - Math.floor(width / 2));
+
+ return {
+ top: top - height,
+ left: Math.min(x, chartBox.width - width)
+ };
+ } catch (e) {
+ }
+ }
+ };
+ };
+
+ /*
+ * Setup Axis options. Default is to not show either axis. This can be overridden in two ways:
+ * 1) in the config, setting showAxis to true will show both axes
+ * 2) in the attributes showXAxis and showYAxis will override the config if set
+ *
+ * By default only line and the tick marks are shown, no labels. This is a sparkline and should be used
+ * only to show a brief idea of trending. This can be overridden by setting the config.axis options per C3
+ */
+
+ if ($scope.showXAxis === undefined) {
+ $scope.showXAxis = ($scope.config.showAxis !== undefined) && $scope.config.showAxis;
+ }
+
+ if ($scope.showYAxis === undefined) {
+ $scope.showYAxis = ($scope.config.showAxis !== undefined) && $scope.config.showAxis;
+ }
+
+ $scope.defaultConfig = patternfly.c3ChartDefaults().getDefaultSparklineConfig();
+ $scope.defaultConfig.axis = {
+ x: {
+ show: $scope.showXAxis === true,
+ type: 'timeseries',
+ tick: {
+ format: function () {
+ return '';
+ }
+ }
+ },
+ y: {
+ show: $scope.showYAxis === true,
+ tick: {
+ format: function () {
+ return '';
+ }
+ }
+ }
+ };
+
+ // Setup the default configuration
+ $scope.defaultConfig.tooltip = $scope.sparklineTooltip();
+ if ($scope.chartHeight) {
+ $scope.defaultConfig.size.height = $scope.chartHeight;
+ }
+ $scope.defaultConfig.units = '';
+
+ // Convert the given data to C3 chart format
+ $scope.config.data = pfUtils.merge($scope.config.data, $scope.getSparklineData($scope.chartData));
+
+ // Override defaults with callers specifications
+ $scope.chartConfig = pfUtils.merge($scope.defaultConfig, $scope.config);
+ }
+ ],
+
+ link: function (scope) {
+ scope.$watch('config', function () {
+ scope.config.data = pfUtils.merge(scope.config.data, scope.getSparklineData(scope.chartData));
+ scope.chartConfig = pfUtils.merge(scope.defaultConfig, scope.config);
+ }, true);
+ scope.$watch('chartHeight', function () {
+ if (scope.chartHeight) {
+ scope.chartConfig.size.height = scope.chartHeight;
+ }
+ });
+ scope.$watch('showXAxis', function () {
+ scope.chartConfig.axis.x.show = scope.showXAxis === true;
+ });
+ scope.$watch('showYAxis', function () {
+ scope.chartConfig.axis.y.show = scope.showYAxis === true;
+ });
+ scope.$watch('chartData', function () {
+ scope.chartConfig.data = pfUtils.merge(scope.chartConfig.data, scope.getSparklineData(scope.chartData));
+ }, true);
+ }
+ };
+ }]);
+}(patternfly));
+;/**
+ * @ngdoc directive
+ * @name patternfly.charts.directive:pfTrendsChart
+ *
+ * @description
+ * Directive for rendering a trend chart. The trend chart combines overall data with a
+ * pfSparklineChart.
+ *
+ * See http://c3js.org/reference.html for a full list of C3 chart options.
+ * See also: {@link patternfly.charts.directive:pfSparklineChart}
+ *
+ * @param {object} config configuration settings for the trends chart:
+ *
+ *
.chartId - the unique id of this trends chart
+ *
.title - (optional) title of the Trends chart
+ *
.layout - (optional) the layout and sizes of titles and chart. Values are 'large' (default), 'small', 'compact', and 'inline'
+ *
.trendLabel - (optional) the trend label used in the 'inline' layout
+ *
.timeFrame - (optional) the time frame for the data in the pfSparklineChart, ex: 'Last 30 Days'
+ *
.units - unit label for values, ex: 'MHz','GB', etc..
+ *
.valueType - (optional) the format of the latest data point which is shown in the title. Values are 'actual'(default) or 'percentage'
+ *
+ *
+ * @param {object} chartData the data to be shown in the sparkline charts
+ *
+ *
.total - number representing the total amount
+ *
.xData - Array, X values for the data points, first element must be the name of the data
+ *
.yData - Array, Y Values for the data points, first element must be the name of the data
+ *
.dataAvailable - Flag if there is data available - default: true
+ *
+ *
+ * @param {int=} chartHeight height of the sparkline chart
+ * @param {boolean=} showXAxis override sparkline config settings for showing the X Axis
+ * @param {boolean=} showYAxis override sparkline config settings for showing the Y Axis
+ * @example
+
+
+
When no thresholds exist, or if the used percentage has not surpassed any thresholds, the indicator is blue.
+ *
When the used percentage has surpassed the warning threshold, but not the error threshold, the indicator is orange.
+ *
When the used percentage has surpassed the error threshold, the indicator is is red.
+ *
+ *
+ * @param {object} chartData the data to be shown in the utilization bar chart
+ *
+ *
.used - number representing the amount used
+ *
.total - number representing the total amount
+ *
.dataAvailable - Flag if there is data available - default: true
+ *
+ *
+ * @param {object=} chart-title The title displayed on the left-hand side of the chart
+ * @param {object=} chart-footer The label displayed on the right-hand side of the chart. If chart-footer is not
+ * specified, the automatic footer-label-format will be used.
+ * @param {object=} layout Various alternative layouts the utilization bar chart may have:
+ *
+ *
.type - The type of layout to use. Valid values are 'regular' (default) displays the standard chart layout,
+ * and 'inline' displays a smaller, inline layout.
+ *
.titleLabelWidth - Width of the left-hand title label when using 'inline' layout. Example values are "120px", "20%", "10em", etc..
+ *
.footerLabelWidth - Width of the right-hand used label when using 'inline' layout. Example values are "120px", "20%", "10em", etc..
+ *
+ * @param {string=} footer-label-format The auto-format of the label on the right side of the bar chart when chart-footer
+ * has not been specified. Values may be:
+ *
+ *
'actual' - (default) displays the standard label of '(n) of (m) (units) Used'.
+ *
'percent' - displays a percentage label of '(n)% Used'.
+ *
+ * @param {object=} units to be displayed on the chart. Examples: "GB", "MHz", "I/Ops", etc...
+ * @param {string=} threshold-error The percentage used, when reached, denotes an error. Valid values are 1-100. When the error threshold
+ * has been reached, the used donut arc will be red.
+ * @param {string=} threshold-warning The percentage usage, when reached, denotes a warning. Valid values are 1-100. When the warning threshold
+ * has been reached, the used donut arc will be orange.
+ *
+ * @example
+
+
+
+ * See http://c3js.org/reference.html for a full list of C3 chart options.
+ *
+ * @param {object} config configuration settings for the utilization trend chart:
+ *
+ *
.title - title of the Utilization chart
+ *
.units - unit label for values, ex: 'MHz','GB', etc..
+ *
+ *
+ * @param {object} donutConfig configuration settings for the donut pct chart, see pfDonutPctChart for specifics
+ * @param {object} sparklineConfig configuration settings for the sparkline chart, see pfSparklineChart for specifics
+ *
+ * @param {object} chartData the data to be shown in the donut and sparkline charts
+ *
+ *
.used - number representing the amount used
+ *
.total - number representing the total amount
+ *
.xData - Array, X values for the data points, first element must be the name of the data
+ *
.yData - Array, Y Values for the data points, first element must be the name of the data
+ *
.dataAvailable - Flag if there is data available - default: true
+ *
+ *
+ * @param {string=} donutCenterLabel specifies the contents of the donut's center label.
+ * Values:
+ *
+ *
'used' - displays the Used amount in the center label (default)
+ *
'available' - displays the Available amount in the center label
+ *
'percent' - displays the Usage Percent of the Total amount in the center label
+ *
'none' - does not display the center label
+ *
+ * @param {int=} sparklineChartHeight height of the sparkline chart
+ * @param {boolean=} showSparklineXAxis override sparkline config settings for showing the X Axis
+ * @param {boolean=} showSparklineYAxis override sparkline config settings for showing the Y Axis
+
+ * @example
+
+
+
+
+
+
+ .container.ng-enter,
+ .container.ng-leave {
+ transition: all ease 1.5s;
+ }
+
+ .container.ng-enter,
+ .container.ng-leave-active {
+ opacity: 0;
+ }
+
+ .container.ng-leave,
+ .container.ng-enter-active {
+ opacity: 1;
+ }
+
+ .item {
+ background: firebrick;
+ color: #FFF;
+ margin-bottom: 10px;
+ }
+
+ .item.ng-enter,
+ .item.ng-leave {
+ transition: transform 1.5s ease;
+ }
+
+ .item.ng-enter {
+ transform: translateX(50px);
+ }
+
+ .item.ng-enter-active {
+ transform: translateX(0);
+ }
+
+
+ angular.module('ngAnimateChildren', ['ngAnimate'])
+ .controller('MainController', function MainController() {
+ this.animateChildren = false;
+ this.enterElement = false;
+ });
+
+
+ */
+var $$AnimateChildrenDirective = ['$interpolate', function($interpolate) {
+ return {
+ link: function(scope, element, attrs) {
+ var val = attrs.ngAnimateChildren;
+ if (isString(val) && val.length === 0) { //empty attribute
+ element.data(NG_ANIMATE_CHILDREN_DATA, true);
+ } else {
+ // Interpolate and set the value, so that it is available to
+ // animations that run right after compilation
+ setData($interpolate(val)(scope));
+ attrs.$observe('ngAnimateChildren', setData);
+ }
+
+ function setData(value) {
+ value = value === 'on' || value === 'true';
+ element.data(NG_ANIMATE_CHILDREN_DATA, value);
+ }
+ }
+ };
+}];
+
+/* exported $AnimateCssProvider */
+
+var ANIMATE_TIMER_KEY = '$$animateCss';
+
+/**
+ * @ngdoc service
+ * @name $animateCss
+ * @kind object
+ *
+ * @description
+ * The `$animateCss` service is a useful utility to trigger customized CSS-based transitions/keyframes
+ * from a JavaScript-based animation or directly from a directive. The purpose of `$animateCss` is NOT
+ * to side-step how `$animate` and ngAnimate work, but the goal is to allow pre-existing animations or
+ * directives to create more complex animations that can be purely driven using CSS code.
+ *
+ * Note that only browsers that support CSS transitions and/or keyframe animations are capable of
+ * rendering animations triggered via `$animateCss` (bad news for IE9 and lower).
+ *
+ * ## Usage
+ * Once again, `$animateCss` is designed to be used inside of a registered JavaScript animation that
+ * is powered by ngAnimate. It is possible to use `$animateCss` directly inside of a directive, however,
+ * any automatic control over cancelling animations and/or preventing animations from being run on
+ * child elements will not be handled by Angular. For this to work as expected, please use `$animate` to
+ * trigger the animation and then setup a JavaScript animation that injects `$animateCss` to trigger
+ * the CSS animation.
+ *
+ * The example below shows how we can create a folding animation on an element using `ng-if`:
+ *
+ * ```html
+ *
+ *
+ * This element will go BOOM
+ *
+ *
+ * ```
+ *
+ * Now we create the **JavaScript animation** that will trigger the CSS transition:
+ *
+ * ```js
+ * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element, doneFn) {
+ * var height = element[0].offsetHeight;
+ * return $animateCss(element, {
+ * from: { height:'0px' },
+ * to: { height:height + 'px' },
+ * duration: 1 // one second
+ * });
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * ## More Advanced Uses
+ *
+ * `$animateCss` is the underlying code that ngAnimate uses to power **CSS-based animations** behind the scenes. Therefore CSS hooks
+ * like `.ng-EVENT`, `.ng-EVENT-active`, `.ng-EVENT-stagger` are all features that can be triggered using `$animateCss` via JavaScript code.
+ *
+ * This also means that just about any combination of adding classes, removing classes, setting styles, dynamically setting a keyframe animation,
+ * applying a hardcoded duration or delay value, changing the animation easing or applying a stagger animation are all options that work with
+ * `$animateCss`. The service itself is smart enough to figure out the combination of options and examine the element styling properties in order
+ * to provide a working animation that will run in CSS.
+ *
+ * The example below showcases a more advanced version of the `.fold-animation` from the example above:
+ *
+ * ```js
+ * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element, doneFn) {
+ * var height = element[0].offsetHeight;
+ * return $animateCss(element, {
+ * addClass: 'red large-text pulse-twice',
+ * easing: 'ease-out',
+ * from: { height:'0px' },
+ * to: { height:height + 'px' },
+ * duration: 1 // one second
+ * });
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * Since we're adding/removing CSS classes then the CSS transition will also pick those up:
+ *
+ * ```css
+ * /* since a hardcoded duration value of 1 was provided in the JavaScript animation code,
+ * the CSS classes below will be transitioned despite them being defined as regular CSS classes */
+ * .red { background:red; }
+ * .large-text { font-size:20px; }
+ *
+ * /* we can also use a keyframe animation and $animateCss will make it work alongside the transition */
+ * .pulse-twice {
+ * animation: 0.5s pulse linear 2;
+ * -webkit-animation: 0.5s pulse linear 2;
+ * }
+ *
+ * @keyframes pulse {
+ * from { transform: scale(0.5); }
+ * to { transform: scale(1.5); }
+ * }
+ *
+ * @-webkit-keyframes pulse {
+ * from { -webkit-transform: scale(0.5); }
+ * to { -webkit-transform: scale(1.5); }
+ * }
+ * ```
+ *
+ * Given this complex combination of CSS classes, styles and options, `$animateCss` will figure everything out and make the animation happen.
+ *
+ * ## How the Options are handled
+ *
+ * `$animateCss` is very versatile and intelligent when it comes to figuring out what configurations to apply to the element to ensure the animation
+ * works with the options provided. Say for example we were adding a class that contained a keyframe value and we wanted to also animate some inline
+ * styles using the `from` and `to` properties.
+ *
+ * ```js
+ * var animator = $animateCss(element, {
+ * from: { background:'red' },
+ * to: { background:'blue' }
+ * });
+ * animator.start();
+ * ```
+ *
+ * ```css
+ * .rotating-animation {
+ * animation:0.5s rotate linear;
+ * -webkit-animation:0.5s rotate linear;
+ * }
+ *
+ * @keyframes rotate {
+ * from { transform: rotate(0deg); }
+ * to { transform: rotate(360deg); }
+ * }
+ *
+ * @-webkit-keyframes rotate {
+ * from { -webkit-transform: rotate(0deg); }
+ * to { -webkit-transform: rotate(360deg); }
+ * }
+ * ```
+ *
+ * The missing pieces here are that we do not have a transition set (within the CSS code nor within the `$animateCss` options) and the duration of the animation is
+ * going to be detected from what the keyframe styles on the CSS class are. In this event, `$animateCss` will automatically create an inline transition
+ * style matching the duration detected from the keyframe style (which is present in the CSS class that is being added) and then prepare both the transition
+ * and keyframe animations to run in parallel on the element. Then when the animation is underway the provided `from` and `to` CSS styles will be applied
+ * and spread across the transition and keyframe animation.
+ *
+ * ## What is returned
+ *
+ * `$animateCss` works in two stages: a preparation phase and an animation phase. Therefore when `$animateCss` is first called it will NOT actually
+ * start the animation. All that is going on here is that the element is being prepared for the animation (which means that the generated CSS classes are
+ * added and removed on the element). Once `$animateCss` is called it will return an object with the following properties:
+ *
+ * ```js
+ * var animator = $animateCss(element, { ... });
+ * ```
+ *
+ * Now what do the contents of our `animator` variable look like:
+ *
+ * ```js
+ * {
+ * // starts the animation
+ * start: Function,
+ *
+ * // ends (aborts) the animation
+ * end: Function
+ * }
+ * ```
+ *
+ * To actually start the animation we need to run `animation.start()` which will then return a promise that we can hook into to detect when the animation ends.
+ * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and styles may have been
+ * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties
+ * and that changing them will not reconfigure the parameters of the animation.
+ *
+ * ### runner.done() vs runner.then()
+ * It is documented that `animation.start()` will return a promise object and this is true, however, there is also an additional method available on the
+ * runner called `.done(callbackFn)`. The done method works the same as `.finally(callbackFn)`, however, it does **not trigger a digest to occur**.
+ * Therefore, for performance reasons, it's always best to use `runner.done(callback)` instead of `runner.then()`, `runner.catch()` or `runner.finally()`
+ * unless you really need a digest to kick off afterwards.
+ *
+ * Keep in mind that, to make this easier, ngAnimate has tweaked the JS animations API to recognize when a runner instance is returned from $animateCss
+ * (so there is no need to call `runner.done(doneFn)` inside of your JavaScript animation code).
+ * Check the {@link ngAnimate.$animateCss#usage animation code above} to see how this works.
+ *
+ * @param {DOMElement} element the element that will be animated
+ * @param {object} options the animation-related options that will be applied during the animation
+ *
+ * * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied
+ * to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.)
+ * * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and
+ * `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted.
+ * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both).
+ * * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`).
+ * * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`).
+ * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation.
+ * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition.
+ * * `addClass` - A space separated list of CSS classes that will be added to the element and spread across the animation.
+ * * `removeClass` - A space separated list of CSS classes that will be removed from the element and spread across the animation.
+ * * `duration` - A number value representing the total duration of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `0`
+ * is provided then the animation will be skipped entirely.
+ * * `delay` - A number value representing the total delay of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `true` is
+ * used then whatever delay value is detected from the CSS classes will be mirrored on the elements styles (e.g. by setting delay true then the style value
+ * of the element will be `transition-delay: DETECTED_VALUE`). Using `true` is useful when you want the CSS classes and inline styles to all share the same
+ * CSS delay value.
+ * * `stagger` - A numeric time value representing the delay between successively animated elements
+ * ({@link ngAnimate#css-staggering-animations Click here to learn how CSS-based staggering works in ngAnimate.})
+ * * `staggerIndex` - The numeric index representing the stagger item (e.g. a value of 5 is equal to the sixth item in the stagger; therefore when a
+ * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`)
+ * * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occurring on the classes being added and removed.)
+ * * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once
+ * the animation is closed. This is useful for when the styles are used purely for the sake of
+ * the animation and do not have a lasting visual effect on the element (e.g. a collapse and open animation).
+ * By default this value is set to `false`.
+ *
+ * @return {object} an object with start and end methods and details about the animation.
+ *
+ * * `start` - The method to start the animation. This will return a `Promise` when called.
+ * * `end` - This method will cancel the animation and remove all applied CSS classes and styles.
+ */
+var ONE_SECOND = 1000;
+
+var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
+var CLOSING_TIME_BUFFER = 1.5;
+
+var DETECT_CSS_PROPERTIES = {
+ transitionDuration: TRANSITION_DURATION_PROP,
+ transitionDelay: TRANSITION_DELAY_PROP,
+ transitionProperty: TRANSITION_PROP + PROPERTY_KEY,
+ animationDuration: ANIMATION_DURATION_PROP,
+ animationDelay: ANIMATION_DELAY_PROP,
+ animationIterationCount: ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY
+};
+
+var DETECT_STAGGER_CSS_PROPERTIES = {
+ transitionDuration: TRANSITION_DURATION_PROP,
+ transitionDelay: TRANSITION_DELAY_PROP,
+ animationDuration: ANIMATION_DURATION_PROP,
+ animationDelay: ANIMATION_DELAY_PROP
+};
+
+function getCssKeyframeDurationStyle(duration) {
+ return [ANIMATION_DURATION_PROP, duration + 's'];
+}
+
+function getCssDelayStyle(delay, isKeyframeAnimation) {
+ var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP;
+ return [prop, delay + 's'];
+}
+
+function computeCssStyles($window, element, properties) {
+ var styles = Object.create(null);
+ var detectedStyles = $window.getComputedStyle(element) || {};
+ forEach(properties, function(formalStyleName, actualStyleName) {
+ var val = detectedStyles[formalStyleName];
+ if (val) {
+ var c = val.charAt(0);
+
+ // only numerical-based values have a negative sign or digit as the first value
+ if (c === '-' || c === '+' || c >= 0) {
+ val = parseMaxTime(val);
+ }
+
+ // by setting this to null in the event that the delay is not set or is set directly as 0
+ // then we can still allow for negative values to be used later on and not mistake this
+ // value for being greater than any other negative value.
+ if (val === 0) {
+ val = null;
+ }
+ styles[actualStyleName] = val;
+ }
+ });
+
+ return styles;
+}
+
+function parseMaxTime(str) {
+ var maxValue = 0;
+ var values = str.split(/\s*,\s*/);
+ forEach(values, function(value) {
+ // it's always safe to consider only second values and omit `ms` values since
+ // getComputedStyle will always handle the conversion for us
+ if (value.charAt(value.length - 1) === 's') {
+ value = value.substring(0, value.length - 1);
+ }
+ value = parseFloat(value) || 0;
+ maxValue = maxValue ? Math.max(value, maxValue) : value;
+ });
+ return maxValue;
+}
+
+function truthyTimingValue(val) {
+ return val === 0 || val != null;
+}
+
+function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
+ var style = TRANSITION_PROP;
+ var value = duration + 's';
+ if (applyOnlyDuration) {
+ style += DURATION_KEY;
+ } else {
+ value += ' linear all';
+ }
+ return [style, value];
+}
+
+function createLocalCacheLookup() {
+ var cache = Object.create(null);
+ return {
+ flush: function() {
+ cache = Object.create(null);
+ },
+
+ count: function(key) {
+ var entry = cache[key];
+ return entry ? entry.total : 0;
+ },
+
+ get: function(key) {
+ var entry = cache[key];
+ return entry && entry.value;
+ },
+
+ put: function(key, value) {
+ if (!cache[key]) {
+ cache[key] = { total: 1, value: value };
+ } else {
+ cache[key].total++;
+ }
+ }
+ };
+}
+
+// we do not reassign an already present style value since
+// if we detect the style property value again we may be
+// detecting styles that were added via the `from` styles.
+// We make use of `isDefined` here since an empty string
+// or null value (which is what getPropertyValue will return
+// for a non-existing style) will still be marked as a valid
+// value for the style (a falsy value implies that the style
+// is to be removed at the end of the animation). If we had a simple
+// "OR" statement then it would not be enough to catch that.
+function registerRestorableStyles(backup, node, properties) {
+ forEach(properties, function(prop) {
+ backup[prop] = isDefined(backup[prop])
+ ? backup[prop]
+ : node.style.getPropertyValue(prop);
+ });
+}
+
+var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animateProvider) {
+ var gcsLookup = createLocalCacheLookup();
+ var gcsStaggerLookup = createLocalCacheLookup();
+
+ this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
+ '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue',
+ function($window, $$jqLite, $$AnimateRunner, $timeout,
+ $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) {
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ var parentCounter = 0;
+ function gcsHashFn(node, extraClasses) {
+ var KEY = '$$ngAnimateParentKey';
+ var parentNode = node.parentNode;
+ var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
+ return parentID + '-' + node.getAttribute('class') + '-' + extraClasses;
+ }
+
+ function computeCachedCssStyles(node, className, cacheKey, properties) {
+ var timings = gcsLookup.get(cacheKey);
+
+ if (!timings) {
+ timings = computeCssStyles($window, node, properties);
+ if (timings.animationIterationCount === 'infinite') {
+ timings.animationIterationCount = 1;
+ }
+ }
+
+ // we keep putting this in multiple times even though the value and the cacheKey are the same
+ // because we're keeping an internal tally of how many duplicate animations are detected.
+ gcsLookup.put(cacheKey, timings);
+ return timings;
+ }
+
+ function computeCachedCssStaggerStyles(node, className, cacheKey, properties) {
+ var stagger;
+
+ // if we have one or more existing matches of matching elements
+ // containing the same parent + CSS styles (which is how cacheKey works)
+ // then staggering is possible
+ if (gcsLookup.count(cacheKey) > 0) {
+ stagger = gcsStaggerLookup.get(cacheKey);
+
+ if (!stagger) {
+ var staggerClassName = pendClasses(className, '-stagger');
+
+ $$jqLite.addClass(node, staggerClassName);
+
+ stagger = computeCssStyles($window, node, properties);
+
+ // force the conversion of a null value to zero incase not set
+ stagger.animationDuration = Math.max(stagger.animationDuration, 0);
+ stagger.transitionDuration = Math.max(stagger.transitionDuration, 0);
+
+ $$jqLite.removeClass(node, staggerClassName);
+
+ gcsStaggerLookup.put(cacheKey, stagger);
+ }
+ }
+
+ return stagger || {};
+ }
+
+ var rafWaitQueue = [];
+ function waitUntilQuiet(callback) {
+ rafWaitQueue.push(callback);
+ $$rAFScheduler.waitUntilQuiet(function() {
+ gcsLookup.flush();
+ gcsStaggerLookup.flush();
+
+ // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
+ // PLEASE EXAMINE THE `$$forceReflow` service to understand why.
+ var pageWidth = $$forceReflow();
+
+ // we use a for loop to ensure that if the queue is changed
+ // during this looping then it will consider new requests
+ for (var i = 0; i < rafWaitQueue.length; i++) {
+ rafWaitQueue[i](pageWidth);
+ }
+ rafWaitQueue.length = 0;
+ });
+ }
+
+ function computeTimings(node, className, cacheKey) {
+ var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES);
+ var aD = timings.animationDelay;
+ var tD = timings.transitionDelay;
+ timings.maxDelay = aD && tD
+ ? Math.max(aD, tD)
+ : (aD || tD);
+ timings.maxDuration = Math.max(
+ timings.animationDuration * timings.animationIterationCount,
+ timings.transitionDuration);
+
+ return timings;
+ }
+
+ return function init(element, initialOptions) {
+ // all of the animation functions should create
+ // a copy of the options data, however, if a
+ // parent service has already created a copy then
+ // we should stick to using that
+ var options = initialOptions || {};
+ if (!options.$$prepared) {
+ options = prepareAnimationOptions(copy(options));
+ }
+
+ var restoreStyles = {};
+ var node = getDomNode(element);
+ if (!node
+ || !node.parentNode
+ || !$$animateQueue.enabled()) {
+ return closeAndReturnNoopAnimator();
+ }
+
+ var temporaryStyles = [];
+ var classes = element.attr('class');
+ var styles = packageStyles(options);
+ var animationClosed;
+ var animationPaused;
+ var animationCompleted;
+ var runner;
+ var runnerHost;
+ var maxDelay;
+ var maxDelayTime;
+ var maxDuration;
+ var maxDurationTime;
+ var startTime;
+ var events = [];
+
+ if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) {
+ return closeAndReturnNoopAnimator();
+ }
+
+ var method = options.event && isArray(options.event)
+ ? options.event.join(' ')
+ : options.event;
+
+ var isStructural = method && options.structural;
+ var structuralClassName = '';
+ var addRemoveClassName = '';
+
+ if (isStructural) {
+ structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true);
+ } else if (method) {
+ structuralClassName = method;
+ }
+
+ if (options.addClass) {
+ addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX);
+ }
+
+ if (options.removeClass) {
+ if (addRemoveClassName.length) {
+ addRemoveClassName += ' ';
+ }
+ addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX);
+ }
+
+ // there may be a situation where a structural animation is combined together
+ // with CSS classes that need to resolve before the animation is computed.
+ // However this means that there is no explicit CSS code to block the animation
+ // from happening (by setting 0s none in the class name). If this is the case
+ // we need to apply the classes before the first rAF so we know to continue if
+ // there actually is a detected transition or keyframe animation
+ if (options.applyClassesEarly && addRemoveClassName.length) {
+ applyAnimationClasses(element, options);
+ }
+
+ var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
+ var fullClassName = classes + ' ' + preparationClasses;
+ var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
+ var hasToStyles = styles.to && Object.keys(styles.to).length > 0;
+ var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0;
+
+ // there is no way we can trigger an animation if no styles and
+ // no classes are being applied which would then trigger a transition,
+ // unless there a is raw keyframe value that is applied to the element.
+ if (!containsKeyframeAnimation
+ && !hasToStyles
+ && !preparationClasses) {
+ return closeAndReturnNoopAnimator();
+ }
+
+ var cacheKey, stagger;
+ if (options.stagger > 0) {
+ var staggerVal = parseFloat(options.stagger);
+ stagger = {
+ transitionDelay: staggerVal,
+ animationDelay: staggerVal,
+ transitionDuration: 0,
+ animationDuration: 0
+ };
+ } else {
+ cacheKey = gcsHashFn(node, fullClassName);
+ stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
+ }
+
+ if (!options.$$skipPreparationClasses) {
+ $$jqLite.addClass(element, preparationClasses);
+ }
+
+ var applyOnlyDuration;
+
+ if (options.transitionStyle) {
+ var transitionStyle = [TRANSITION_PROP, options.transitionStyle];
+ applyInlineStyle(node, transitionStyle);
+ temporaryStyles.push(transitionStyle);
+ }
+
+ if (options.duration >= 0) {
+ applyOnlyDuration = node.style[TRANSITION_PROP].length > 0;
+ var durationStyle = getCssTransitionDurationStyle(options.duration, applyOnlyDuration);
+
+ // we set the duration so that it will be picked up by getComputedStyle later
+ applyInlineStyle(node, durationStyle);
+ temporaryStyles.push(durationStyle);
+ }
+
+ if (options.keyframeStyle) {
+ var keyframeStyle = [ANIMATION_PROP, options.keyframeStyle];
+ applyInlineStyle(node, keyframeStyle);
+ temporaryStyles.push(keyframeStyle);
+ }
+
+ var itemIndex = stagger
+ ? options.staggerIndex >= 0
+ ? options.staggerIndex
+ : gcsLookup.count(cacheKey)
+ : 0;
+
+ var isFirst = itemIndex === 0;
+
+ // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY
+ // without causing any combination of transitions to kick in. By adding a negative delay value
+ // it forces the setup class' transition to end immediately. We later then remove the negative
+ // transition delay to allow for the transition to naturally do it's thing. The beauty here is
+ // that if there is no transition defined then nothing will happen and this will also allow
+ // other transitions to be stacked on top of each other without any chopping them out.
+ if (isFirst && !options.skipBlocking) {
+ blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
+ }
+
+ var timings = computeTimings(node, fullClassName, cacheKey);
+ var relativeDelay = timings.maxDelay;
+ maxDelay = Math.max(relativeDelay, 0);
+ maxDuration = timings.maxDuration;
+
+ var flags = {};
+ flags.hasTransitions = timings.transitionDuration > 0;
+ flags.hasAnimations = timings.animationDuration > 0;
+ flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty === 'all';
+ flags.applyTransitionDuration = hasToStyles && (
+ (flags.hasTransitions && !flags.hasTransitionAll)
+ || (flags.hasAnimations && !flags.hasTransitions));
+ flags.applyAnimationDuration = options.duration && flags.hasAnimations;
+ flags.applyTransitionDelay = truthyTimingValue(options.delay) && (flags.applyTransitionDuration || flags.hasTransitions);
+ flags.applyAnimationDelay = truthyTimingValue(options.delay) && flags.hasAnimations;
+ flags.recalculateTimingStyles = addRemoveClassName.length > 0;
+
+ if (flags.applyTransitionDuration || flags.applyAnimationDuration) {
+ maxDuration = options.duration ? parseFloat(options.duration) : maxDuration;
+
+ if (flags.applyTransitionDuration) {
+ flags.hasTransitions = true;
+ timings.transitionDuration = maxDuration;
+ applyOnlyDuration = node.style[TRANSITION_PROP + PROPERTY_KEY].length > 0;
+ temporaryStyles.push(getCssTransitionDurationStyle(maxDuration, applyOnlyDuration));
+ }
+
+ if (flags.applyAnimationDuration) {
+ flags.hasAnimations = true;
+ timings.animationDuration = maxDuration;
+ temporaryStyles.push(getCssKeyframeDurationStyle(maxDuration));
+ }
+ }
+
+ if (maxDuration === 0 && !flags.recalculateTimingStyles) {
+ return closeAndReturnNoopAnimator();
+ }
+
+ if (options.delay != null) {
+ var delayStyle;
+ if (typeof options.delay !== 'boolean') {
+ delayStyle = parseFloat(options.delay);
+ // number in options.delay means we have to recalculate the delay for the closing timeout
+ maxDelay = Math.max(delayStyle, 0);
+ }
+
+ if (flags.applyTransitionDelay) {
+ temporaryStyles.push(getCssDelayStyle(delayStyle));
+ }
+
+ if (flags.applyAnimationDelay) {
+ temporaryStyles.push(getCssDelayStyle(delayStyle, true));
+ }
+ }
+
+ // we need to recalculate the delay value since we used a pre-emptive negative
+ // delay value and the delay value is required for the final event checking. This
+ // property will ensure that this will happen after the RAF phase has passed.
+ if (options.duration == null && timings.transitionDuration > 0) {
+ flags.recalculateTimingStyles = flags.recalculateTimingStyles || isFirst;
+ }
+
+ maxDelayTime = maxDelay * ONE_SECOND;
+ maxDurationTime = maxDuration * ONE_SECOND;
+ if (!options.skipBlocking) {
+ flags.blockTransition = timings.transitionDuration > 0;
+ flags.blockKeyframeAnimation = timings.animationDuration > 0 &&
+ stagger.animationDelay > 0 &&
+ stagger.animationDuration === 0;
+ }
+
+ if (options.from) {
+ if (options.cleanupStyles) {
+ registerRestorableStyles(restoreStyles, node, Object.keys(options.from));
+ }
+ applyAnimationFromStyles(element, options);
+ }
+
+ if (flags.blockTransition || flags.blockKeyframeAnimation) {
+ applyBlocking(maxDuration);
+ } else if (!options.skipBlocking) {
+ blockTransitions(node, false);
+ }
+
+ // TODO(matsko): for 1.5 change this code to have an animator object for better debugging
+ return {
+ $$willAnimate: true,
+ end: endFn,
+ start: function() {
+ if (animationClosed) return;
+
+ runnerHost = {
+ end: endFn,
+ cancel: cancelFn,
+ resume: null, //this will be set during the start() phase
+ pause: null
+ };
+
+ runner = new $$AnimateRunner(runnerHost);
+
+ waitUntilQuiet(start);
+
+ // we don't have access to pause/resume the animation
+ // since it hasn't run yet. AnimateRunner will therefore
+ // set noop functions for resume and pause and they will
+ // later be overridden once the animation is triggered
+ return runner;
+ }
+ };
+
+ function endFn() {
+ close();
+ }
+
+ function cancelFn() {
+ close(true);
+ }
+
+ function close(rejected) {
+ // if the promise has been called already then we shouldn't close
+ // the animation again
+ if (animationClosed || (animationCompleted && animationPaused)) return;
+ animationClosed = true;
+ animationPaused = false;
+
+ if (!options.$$skipPreparationClasses) {
+ $$jqLite.removeClass(element, preparationClasses);
+ }
+ $$jqLite.removeClass(element, activeClasses);
+
+ blockKeyframeAnimations(node, false);
+ blockTransitions(node, false);
+
+ forEach(temporaryStyles, function(entry) {
+ // There is only one way to remove inline style properties entirely from elements.
+ // By using `removeProperty` this works, but we need to convert camel-cased CSS
+ // styles down to hyphenated values.
+ node.style[entry[0]] = '';
+ });
+
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+
+ if (Object.keys(restoreStyles).length) {
+ forEach(restoreStyles, function(value, prop) {
+ if (value) {
+ node.style.setProperty(prop, value);
+ } else {
+ node.style.removeProperty(prop);
+ }
+ });
+ }
+
+ // the reason why we have this option is to allow a synchronous closing callback
+ // that is fired as SOON as the animation ends (when the CSS is removed) or if
+ // the animation never takes off at all. A good example is a leave animation since
+ // the element must be removed just after the animation is over or else the element
+ // will appear on screen for one animation frame causing an overbearing flicker.
+ if (options.onDone) {
+ options.onDone();
+ }
+
+ if (events && events.length) {
+ // Remove the transitionend / animationend listener(s)
+ element.off(events.join(' '), onAnimationProgress);
+ }
+
+ //Cancel the fallback closing timeout and remove the timer data
+ var animationTimerData = element.data(ANIMATE_TIMER_KEY);
+ if (animationTimerData) {
+ $timeout.cancel(animationTimerData[0].timer);
+ element.removeData(ANIMATE_TIMER_KEY);
+ }
+
+ // if the preparation function fails then the promise is not setup
+ if (runner) {
+ runner.complete(!rejected);
+ }
+ }
+
+ function applyBlocking(duration) {
+ if (flags.blockTransition) {
+ blockTransitions(node, duration);
+ }
+
+ if (flags.blockKeyframeAnimation) {
+ blockKeyframeAnimations(node, !!duration);
+ }
+ }
+
+ function closeAndReturnNoopAnimator() {
+ runner = new $$AnimateRunner({
+ end: endFn,
+ cancel: cancelFn
+ });
+
+ // should flush the cache animation
+ waitUntilQuiet(noop);
+ close();
+
+ return {
+ $$willAnimate: false,
+ start: function() {
+ return runner;
+ },
+ end: endFn
+ };
+ }
+
+ function onAnimationProgress(event) {
+ event.stopPropagation();
+ var ev = event.originalEvent || event;
+
+ // we now always use `Date.now()` due to the recent changes with
+ // event.timeStamp in Firefox, Webkit and Chrome (see #13494 for more info)
+ var timeStamp = ev.$manualTimeStamp || Date.now();
+
+ /* Firefox (or possibly just Gecko) likes to not round values up
+ * when a ms measurement is used for the animation */
+ var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES));
+
+ /* $manualTimeStamp is a mocked timeStamp value which is set
+ * within browserTrigger(). This is only here so that tests can
+ * mock animations properly. Real events fallback to event.timeStamp,
+ * or, if they don't, then a timeStamp is automatically created for them.
+ * We're checking to see if the timeStamp surpasses the expected delay,
+ * but we're using elapsedTime instead of the timeStamp on the 2nd
+ * pre-condition since animationPauseds sometimes close off early */
+ if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
+ // we set this flag to ensure that if the transition is paused then, when resumed,
+ // the animation will automatically close itself since transitions cannot be paused.
+ animationCompleted = true;
+ close();
+ }
+ }
+
+ function start() {
+ if (animationClosed) return;
+ if (!node.parentNode) {
+ close();
+ return;
+ }
+
+ // even though we only pause keyframe animations here the pause flag
+ // will still happen when transitions are used. Only the transition will
+ // not be paused since that is not possible. If the animation ends when
+ // paused then it will not complete until unpaused or cancelled.
+ var playPause = function(playAnimation) {
+ if (!animationCompleted) {
+ animationPaused = !playAnimation;
+ if (timings.animationDuration) {
+ var value = blockKeyframeAnimations(node, animationPaused);
+ if (animationPaused) {
+ temporaryStyles.push(value);
+ } else {
+ removeFromArray(temporaryStyles, value);
+ }
+ }
+ } else if (animationPaused && playAnimation) {
+ animationPaused = false;
+ close();
+ }
+ };
+
+ // checking the stagger duration prevents an accidentally cascade of the CSS delay style
+ // being inherited from the parent. If the transition duration is zero then we can safely
+ // rely that the delay value is an intentional stagger delay style.
+ var maxStagger = itemIndex > 0
+ && ((timings.transitionDuration && stagger.transitionDuration === 0) ||
+ (timings.animationDuration && stagger.animationDuration === 0))
+ && Math.max(stagger.animationDelay, stagger.transitionDelay);
+ if (maxStagger) {
+ $timeout(triggerAnimationStart,
+ Math.floor(maxStagger * itemIndex * ONE_SECOND),
+ false);
+ } else {
+ triggerAnimationStart();
+ }
+
+ // this will decorate the existing promise runner with pause/resume methods
+ runnerHost.resume = function() {
+ playPause(true);
+ };
+
+ runnerHost.pause = function() {
+ playPause(false);
+ };
+
+ function triggerAnimationStart() {
+ // just incase a stagger animation kicks in when the animation
+ // itself was cancelled entirely
+ if (animationClosed) return;
+
+ applyBlocking(false);
+
+ forEach(temporaryStyles, function(entry) {
+ var key = entry[0];
+ var value = entry[1];
+ node.style[key] = value;
+ });
+
+ applyAnimationClasses(element, options);
+ $$jqLite.addClass(element, activeClasses);
+
+ if (flags.recalculateTimingStyles) {
+ fullClassName = node.className + ' ' + preparationClasses;
+ cacheKey = gcsHashFn(node, fullClassName);
+
+ timings = computeTimings(node, fullClassName, cacheKey);
+ relativeDelay = timings.maxDelay;
+ maxDelay = Math.max(relativeDelay, 0);
+ maxDuration = timings.maxDuration;
+
+ if (maxDuration === 0) {
+ close();
+ return;
+ }
+
+ flags.hasTransitions = timings.transitionDuration > 0;
+ flags.hasAnimations = timings.animationDuration > 0;
+ }
+
+ if (flags.applyAnimationDelay) {
+ relativeDelay = typeof options.delay !== 'boolean' && truthyTimingValue(options.delay)
+ ? parseFloat(options.delay)
+ : relativeDelay;
+
+ maxDelay = Math.max(relativeDelay, 0);
+ timings.animationDelay = relativeDelay;
+ delayStyle = getCssDelayStyle(relativeDelay, true);
+ temporaryStyles.push(delayStyle);
+ node.style[delayStyle[0]] = delayStyle[1];
+ }
+
+ maxDelayTime = maxDelay * ONE_SECOND;
+ maxDurationTime = maxDuration * ONE_SECOND;
+
+ if (options.easing) {
+ var easeProp, easeVal = options.easing;
+ if (flags.hasTransitions) {
+ easeProp = TRANSITION_PROP + TIMING_KEY;
+ temporaryStyles.push([easeProp, easeVal]);
+ node.style[easeProp] = easeVal;
+ }
+ if (flags.hasAnimations) {
+ easeProp = ANIMATION_PROP + TIMING_KEY;
+ temporaryStyles.push([easeProp, easeVal]);
+ node.style[easeProp] = easeVal;
+ }
+ }
+
+ if (timings.transitionDuration) {
+ events.push(TRANSITIONEND_EVENT);
+ }
+
+ if (timings.animationDuration) {
+ events.push(ANIMATIONEND_EVENT);
+ }
+
+ startTime = Date.now();
+ var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime;
+ var endTime = startTime + timerTime;
+
+ var animationsData = element.data(ANIMATE_TIMER_KEY) || [];
+ var setupFallbackTimer = true;
+ if (animationsData.length) {
+ var currentTimerData = animationsData[0];
+ setupFallbackTimer = endTime > currentTimerData.expectedEndTime;
+ if (setupFallbackTimer) {
+ $timeout.cancel(currentTimerData.timer);
+ } else {
+ animationsData.push(close);
+ }
+ }
+
+ if (setupFallbackTimer) {
+ var timer = $timeout(onAnimationExpired, timerTime, false);
+ animationsData[0] = {
+ timer: timer,
+ expectedEndTime: endTime
+ };
+ animationsData.push(close);
+ element.data(ANIMATE_TIMER_KEY, animationsData);
+ }
+
+ if (events.length) {
+ element.on(events.join(' '), onAnimationProgress);
+ }
+
+ if (options.to) {
+ if (options.cleanupStyles) {
+ registerRestorableStyles(restoreStyles, node, Object.keys(options.to));
+ }
+ applyAnimationToStyles(element, options);
+ }
+ }
+
+ function onAnimationExpired() {
+ var animationsData = element.data(ANIMATE_TIMER_KEY);
+
+ // this will be false in the event that the element was
+ // removed from the DOM (via a leave animation or something
+ // similar)
+ if (animationsData) {
+ for (var i = 1; i < animationsData.length; i++) {
+ animationsData[i]();
+ }
+ element.removeData(ANIMATE_TIMER_KEY);
+ }
+ }
+ }
+ };
+ }];
+}];
+
+var $$AnimateCssDriverProvider = ['$$animationProvider', /** @this */ function($$animationProvider) {
+ $$animationProvider.drivers.push('$$animateCssDriver');
+
+ var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim';
+ var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor';
+
+ var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out';
+ var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in';
+
+ function isDocumentFragment(node) {
+ return node.parentNode && node.parentNode.nodeType === 11;
+ }
+
+ this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document',
+ function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) {
+
+ // only browsers that support these properties can render animations
+ if (!$sniffer.animations && !$sniffer.transitions) return noop;
+
+ var bodyNode = $document[0].body;
+ var rootNode = getDomNode($rootElement);
+
+ var rootBodyElement = jqLite(
+ // this is to avoid using something that exists outside of the body
+ // we also special case the doc fragment case because our unit test code
+ // appends the $rootElement to the body after the app has been bootstrapped
+ isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode
+ );
+
+ return function initDriverFn(animationDetails) {
+ return animationDetails.from && animationDetails.to
+ ? prepareFromToAnchorAnimation(animationDetails.from,
+ animationDetails.to,
+ animationDetails.classes,
+ animationDetails.anchors)
+ : prepareRegularAnimation(animationDetails);
+ };
+
+ function filterCssClasses(classes) {
+ //remove all the `ng-` stuff
+ return classes.replace(/\bng-\S+\b/g, '');
+ }
+
+ function getUniqueValues(a, b) {
+ if (isString(a)) a = a.split(' ');
+ if (isString(b)) b = b.split(' ');
+ return a.filter(function(val) {
+ return b.indexOf(val) === -1;
+ }).join(' ');
+ }
+
+ function prepareAnchoredAnimation(classes, outAnchor, inAnchor) {
+ var clone = jqLite(getDomNode(outAnchor).cloneNode(true));
+ var startingClasses = filterCssClasses(getClassVal(clone));
+
+ outAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
+ inAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
+
+ clone.addClass(NG_ANIMATE_ANCHOR_CLASS_NAME);
+
+ rootBodyElement.append(clone);
+
+ var animatorIn, animatorOut = prepareOutAnimation();
+
+ // the user may not end up using the `out` animation and
+ // only making use of the `in` animation or vice-versa.
+ // In either case we should allow this and not assume the
+ // animation is over unless both animations are not used.
+ if (!animatorOut) {
+ animatorIn = prepareInAnimation();
+ if (!animatorIn) {
+ return end();
+ }
+ }
+
+ var startingAnimator = animatorOut || animatorIn;
+
+ return {
+ start: function() {
+ var runner;
+
+ var currentAnimation = startingAnimator.start();
+ currentAnimation.done(function() {
+ currentAnimation = null;
+ if (!animatorIn) {
+ animatorIn = prepareInAnimation();
+ if (animatorIn) {
+ currentAnimation = animatorIn.start();
+ currentAnimation.done(function() {
+ currentAnimation = null;
+ end();
+ runner.complete();
+ });
+ return currentAnimation;
+ }
+ }
+ // in the event that there is no `in` animation
+ end();
+ runner.complete();
+ });
+
+ runner = new $$AnimateRunner({
+ end: endFn,
+ cancel: endFn
+ });
+
+ return runner;
+
+ function endFn() {
+ if (currentAnimation) {
+ currentAnimation.end();
+ }
+ }
+ }
+ };
+
+ function calculateAnchorStyles(anchor) {
+ var styles = {};
+
+ var coords = getDomNode(anchor).getBoundingClientRect();
+
+ // we iterate directly since safari messes up and doesn't return
+ // all the keys for the coords object when iterated
+ forEach(['width','height','top','left'], function(key) {
+ var value = coords[key];
+ switch (key) {
+ case 'top':
+ value += bodyNode.scrollTop;
+ break;
+ case 'left':
+ value += bodyNode.scrollLeft;
+ break;
+ }
+ styles[key] = Math.floor(value) + 'px';
+ });
+ return styles;
+ }
+
+ function prepareOutAnimation() {
+ var animator = $animateCss(clone, {
+ addClass: NG_OUT_ANCHOR_CLASS_NAME,
+ delay: true,
+ from: calculateAnchorStyles(outAnchor)
+ });
+
+ // read the comment within `prepareRegularAnimation` to understand
+ // why this check is necessary
+ return animator.$$willAnimate ? animator : null;
+ }
+
+ function getClassVal(element) {
+ return element.attr('class') || '';
+ }
+
+ function prepareInAnimation() {
+ var endingClasses = filterCssClasses(getClassVal(inAnchor));
+ var toAdd = getUniqueValues(endingClasses, startingClasses);
+ var toRemove = getUniqueValues(startingClasses, endingClasses);
+
+ var animator = $animateCss(clone, {
+ to: calculateAnchorStyles(inAnchor),
+ addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + toAdd,
+ removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + toRemove,
+ delay: true
+ });
+
+ // read the comment within `prepareRegularAnimation` to understand
+ // why this check is necessary
+ return animator.$$willAnimate ? animator : null;
+ }
+
+ function end() {
+ clone.remove();
+ outAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME);
+ inAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME);
+ }
+ }
+
+ function prepareFromToAnchorAnimation(from, to, classes, anchors) {
+ var fromAnimation = prepareRegularAnimation(from, noop);
+ var toAnimation = prepareRegularAnimation(to, noop);
+
+ var anchorAnimations = [];
+ forEach(anchors, function(anchor) {
+ var outElement = anchor['out'];
+ var inElement = anchor['in'];
+ var animator = prepareAnchoredAnimation(classes, outElement, inElement);
+ if (animator) {
+ anchorAnimations.push(animator);
+ }
+ });
+
+ // no point in doing anything when there are no elements to animate
+ if (!fromAnimation && !toAnimation && anchorAnimations.length === 0) return;
+
+ return {
+ start: function() {
+ var animationRunners = [];
+
+ if (fromAnimation) {
+ animationRunners.push(fromAnimation.start());
+ }
+
+ if (toAnimation) {
+ animationRunners.push(toAnimation.start());
+ }
+
+ forEach(anchorAnimations, function(animation) {
+ animationRunners.push(animation.start());
+ });
+
+ var runner = new $$AnimateRunner({
+ end: endFn,
+ cancel: endFn // CSS-driven animations cannot be cancelled, only ended
+ });
+
+ $$AnimateRunner.all(animationRunners, function(status) {
+ runner.complete(status);
+ });
+
+ return runner;
+
+ function endFn() {
+ forEach(animationRunners, function(runner) {
+ runner.end();
+ });
+ }
+ }
+ };
+ }
+
+ function prepareRegularAnimation(animationDetails) {
+ var element = animationDetails.element;
+ var options = animationDetails.options || {};
+
+ if (animationDetails.structural) {
+ options.event = animationDetails.event;
+ options.structural = true;
+ options.applyClassesEarly = true;
+
+ // we special case the leave animation since we want to ensure that
+ // the element is removed as soon as the animation is over. Otherwise
+ // a flicker might appear or the element may not be removed at all
+ if (animationDetails.event === 'leave') {
+ options.onDone = options.domOperation;
+ }
+ }
+
+ // We assign the preparationClasses as the actual animation event since
+ // the internals of $animateCss will just suffix the event token values
+ // with `-active` to trigger the animation.
+ if (options.preparationClasses) {
+ options.event = concatWithSpace(options.event, options.preparationClasses);
+ }
+
+ var animator = $animateCss(element, options);
+
+ // the driver lookup code inside of $$animation attempts to spawn a
+ // driver one by one until a driver returns a.$$willAnimate animator object.
+ // $animateCss will always return an object, however, it will pass in
+ // a flag as a hint as to whether an animation was detected or not
+ return animator.$$willAnimate ? animator : null;
+ }
+ }];
+}];
+
+// TODO(matsko): use caching here to speed things up for detection
+// TODO(matsko): add documentation
+// by the time...
+
+var $$AnimateJsProvider = ['$animateProvider', /** @this */ function($animateProvider) {
+ this.$get = ['$injector', '$$AnimateRunner', '$$jqLite',
+ function($injector, $$AnimateRunner, $$jqLite) {
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+ // $animateJs(element, 'enter');
+ return function(element, event, classes, options) {
+ var animationClosed = false;
+
+ // the `classes` argument is optional and if it is not used
+ // then the classes will be resolved from the element's className
+ // property as well as options.addClass/options.removeClass.
+ if (arguments.length === 3 && isObject(classes)) {
+ options = classes;
+ classes = null;
+ }
+
+ options = prepareAnimationOptions(options);
+ if (!classes) {
+ classes = element.attr('class') || '';
+ if (options.addClass) {
+ classes += ' ' + options.addClass;
+ }
+ if (options.removeClass) {
+ classes += ' ' + options.removeClass;
+ }
+ }
+
+ var classesToAdd = options.addClass;
+ var classesToRemove = options.removeClass;
+
+ // the lookupAnimations function returns a series of animation objects that are
+ // matched up with one or more of the CSS classes. These animation objects are
+ // defined via the module.animation factory function. If nothing is detected then
+ // we don't return anything which then makes $animation query the next driver.
+ var animations = lookupAnimations(classes);
+ var before, after;
+ if (animations.length) {
+ var afterFn, beforeFn;
+ if (event === 'leave') {
+ beforeFn = 'leave';
+ afterFn = 'afterLeave'; // TODO(matsko): get rid of this
+ } else {
+ beforeFn = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ afterFn = event;
+ }
+
+ if (event !== 'enter' && event !== 'move') {
+ before = packageAnimations(element, event, options, animations, beforeFn);
+ }
+ after = packageAnimations(element, event, options, animations, afterFn);
+ }
+
+ // no matching animations
+ if (!before && !after) return;
+
+ function applyOptions() {
+ options.domOperation();
+ applyAnimationClasses(element, options);
+ }
+
+ function close() {
+ animationClosed = true;
+ applyOptions();
+ applyAnimationStyles(element, options);
+ }
+
+ var runner;
+
+ return {
+ $$willAnimate: true,
+ end: function() {
+ if (runner) {
+ runner.end();
+ } else {
+ close();
+ runner = new $$AnimateRunner();
+ runner.complete(true);
+ }
+ return runner;
+ },
+ start: function() {
+ if (runner) {
+ return runner;
+ }
+
+ runner = new $$AnimateRunner();
+ var closeActiveAnimations;
+ var chain = [];
+
+ if (before) {
+ chain.push(function(fn) {
+ closeActiveAnimations = before(fn);
+ });
+ }
+
+ if (chain.length) {
+ chain.push(function(fn) {
+ applyOptions();
+ fn(true);
+ });
+ } else {
+ applyOptions();
+ }
+
+ if (after) {
+ chain.push(function(fn) {
+ closeActiveAnimations = after(fn);
+ });
+ }
+
+ runner.setHost({
+ end: function() {
+ endAnimations();
+ },
+ cancel: function() {
+ endAnimations(true);
+ }
+ });
+
+ $$AnimateRunner.chain(chain, onComplete);
+ return runner;
+
+ function onComplete(success) {
+ close(success);
+ runner.complete(success);
+ }
+
+ function endAnimations(cancelled) {
+ if (!animationClosed) {
+ (closeActiveAnimations || noop)(cancelled);
+ onComplete(cancelled);
+ }
+ }
+ }
+ };
+
+ function executeAnimationFn(fn, element, event, options, onDone) {
+ var args;
+ switch (event) {
+ case 'animate':
+ args = [element, options.from, options.to, onDone];
+ break;
+
+ case 'setClass':
+ args = [element, classesToAdd, classesToRemove, onDone];
+ break;
+
+ case 'addClass':
+ args = [element, classesToAdd, onDone];
+ break;
+
+ case 'removeClass':
+ args = [element, classesToRemove, onDone];
+ break;
+
+ default:
+ args = [element, onDone];
+ break;
+ }
+
+ args.push(options);
+
+ var value = fn.apply(fn, args);
+ if (value) {
+ if (isFunction(value.start)) {
+ value = value.start();
+ }
+
+ if (value instanceof $$AnimateRunner) {
+ value.done(onDone);
+ } else if (isFunction(value)) {
+ // optional onEnd / onCancel callback
+ return value;
+ }
+ }
+
+ return noop;
+ }
+
+ function groupEventedAnimations(element, event, options, animations, fnName) {
+ var operations = [];
+ forEach(animations, function(ani) {
+ var animation = ani[fnName];
+ if (!animation) return;
+
+ // note that all of these animations will run in parallel
+ operations.push(function() {
+ var runner;
+ var endProgressCb;
+
+ var resolved = false;
+ var onAnimationComplete = function(rejected) {
+ if (!resolved) {
+ resolved = true;
+ (endProgressCb || noop)(rejected);
+ runner.complete(!rejected);
+ }
+ };
+
+ runner = new $$AnimateRunner({
+ end: function() {
+ onAnimationComplete();
+ },
+ cancel: function() {
+ onAnimationComplete(true);
+ }
+ });
+
+ endProgressCb = executeAnimationFn(animation, element, event, options, function(result) {
+ var cancelled = result === false;
+ onAnimationComplete(cancelled);
+ });
+
+ return runner;
+ });
+ });
+
+ return operations;
+ }
+
+ function packageAnimations(element, event, options, animations, fnName) {
+ var operations = groupEventedAnimations(element, event, options, animations, fnName);
+ if (operations.length === 0) {
+ var a, b;
+ if (fnName === 'beforeSetClass') {
+ a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass');
+ b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass');
+ } else if (fnName === 'setClass') {
+ a = groupEventedAnimations(element, 'removeClass', options, animations, 'removeClass');
+ b = groupEventedAnimations(element, 'addClass', options, animations, 'addClass');
+ }
+
+ if (a) {
+ operations = operations.concat(a);
+ }
+ if (b) {
+ operations = operations.concat(b);
+ }
+ }
+
+ if (operations.length === 0) return;
+
+ // TODO(matsko): add documentation
+ return function startAnimation(callback) {
+ var runners = [];
+ if (operations.length) {
+ forEach(operations, function(animateFn) {
+ runners.push(animateFn());
+ });
+ }
+
+ if (runners.length) {
+ $$AnimateRunner.all(runners, callback);
+ } else {
+ callback();
+ }
+
+ return function endFn(reject) {
+ forEach(runners, function(runner) {
+ if (reject) {
+ runner.cancel();
+ } else {
+ runner.end();
+ }
+ });
+ };
+ };
+ }
+ };
+
+ function lookupAnimations(classes) {
+ classes = isArray(classes) ? classes : classes.split(' ');
+ var matches = [], flagMap = {};
+ for (var i = 0; i < classes.length; i++) {
+ var klass = classes[i],
+ animationFactory = $animateProvider.$$registeredAnimations[klass];
+ if (animationFactory && !flagMap[klass]) {
+ matches.push($injector.get(animationFactory));
+ flagMap[klass] = true;
+ }
+ }
+ return matches;
+ }
+ }];
+}];
+
+var $$AnimateJsDriverProvider = ['$$animationProvider', /** @this */ function($$animationProvider) {
+ $$animationProvider.drivers.push('$$animateJsDriver');
+ this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) {
+ return function initDriverFn(animationDetails) {
+ if (animationDetails.from && animationDetails.to) {
+ var fromAnimation = prepareAnimation(animationDetails.from);
+ var toAnimation = prepareAnimation(animationDetails.to);
+ if (!fromAnimation && !toAnimation) return;
+
+ return {
+ start: function() {
+ var animationRunners = [];
+
+ if (fromAnimation) {
+ animationRunners.push(fromAnimation.start());
+ }
+
+ if (toAnimation) {
+ animationRunners.push(toAnimation.start());
+ }
+
+ $$AnimateRunner.all(animationRunners, done);
+
+ var runner = new $$AnimateRunner({
+ end: endFnFactory(),
+ cancel: endFnFactory()
+ });
+
+ return runner;
+
+ function endFnFactory() {
+ return function() {
+ forEach(animationRunners, function(runner) {
+ // at this point we cannot cancel animations for groups just yet. 1.5+
+ runner.end();
+ });
+ };
+ }
+
+ function done(status) {
+ runner.complete(status);
+ }
+ }
+ };
+ } else {
+ return prepareAnimation(animationDetails);
+ }
+ };
+
+ function prepareAnimation(animationDetails) {
+ // TODO(matsko): make sure to check for grouped animations and delegate down to normal animations
+ var element = animationDetails.element;
+ var event = animationDetails.event;
+ var options = animationDetails.options;
+ var classes = animationDetails.classes;
+ return $$animateJs(element, event, classes, options);
+ }
+ }];
+}];
+
+var NG_ANIMATE_ATTR_NAME = 'data-ng-animate';
+var NG_ANIMATE_PIN_DATA = '$ngAnimatePin';
+var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animateProvider) {
+ var PRE_DIGEST_STATE = 1;
+ var RUNNING_STATE = 2;
+ var ONE_SPACE = ' ';
+
+ var rules = this.rules = {
+ skip: [],
+ cancel: [],
+ join: []
+ };
+
+ function makeTruthyCssClassMap(classString) {
+ if (!classString) {
+ return null;
+ }
+
+ var keys = classString.split(ONE_SPACE);
+ var map = Object.create(null);
+
+ forEach(keys, function(key) {
+ map[key] = true;
+ });
+ return map;
+ }
+
+ function hasMatchingClasses(newClassString, currentClassString) {
+ if (newClassString && currentClassString) {
+ var currentClassMap = makeTruthyCssClassMap(currentClassString);
+ return newClassString.split(ONE_SPACE).some(function(className) {
+ return currentClassMap[className];
+ });
+ }
+ }
+
+ function isAllowed(ruleType, element, currentAnimation, previousAnimation) {
+ return rules[ruleType].some(function(fn) {
+ return fn(element, currentAnimation, previousAnimation);
+ });
+ }
+
+ function hasAnimationClasses(animation, and) {
+ var a = (animation.addClass || '').length > 0;
+ var b = (animation.removeClass || '').length > 0;
+ return and ? a && b : a || b;
+ }
+
+ rules.join.push(function(element, newAnimation, currentAnimation) {
+ // if the new animation is class-based then we can just tack that on
+ return !newAnimation.structural && hasAnimationClasses(newAnimation);
+ });
+
+ rules.skip.push(function(element, newAnimation, currentAnimation) {
+ // there is no need to animate anything if no classes are being added and
+ // there is no structural animation that will be triggered
+ return !newAnimation.structural && !hasAnimationClasses(newAnimation);
+ });
+
+ rules.skip.push(function(element, newAnimation, currentAnimation) {
+ // why should we trigger a new structural animation if the element will
+ // be removed from the DOM anyway?
+ return currentAnimation.event === 'leave' && newAnimation.structural;
+ });
+
+ rules.skip.push(function(element, newAnimation, currentAnimation) {
+ // if there is an ongoing current animation then don't even bother running the class-based animation
+ return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural;
+ });
+
+ rules.cancel.push(function(element, newAnimation, currentAnimation) {
+ // there can never be two structural animations running at the same time
+ return currentAnimation.structural && newAnimation.structural;
+ });
+
+ rules.cancel.push(function(element, newAnimation, currentAnimation) {
+ // if the previous animation is already running, but the new animation will
+ // be triggered, but the new animation is structural
+ return currentAnimation.state === RUNNING_STATE && newAnimation.structural;
+ });
+
+ rules.cancel.push(function(element, newAnimation, currentAnimation) {
+ // cancel the animation if classes added / removed in both animation cancel each other out,
+ // but only if the current animation isn't structural
+
+ if (currentAnimation.structural) return false;
+
+ var nA = newAnimation.addClass;
+ var nR = newAnimation.removeClass;
+ var cA = currentAnimation.addClass;
+ var cR = currentAnimation.removeClass;
+
+ // early detection to save the global CPU shortage :)
+ if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) {
+ return false;
+ }
+
+ return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA);
+ });
+
+ this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
+ '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow',
+ function($$rAF, $rootScope, $rootElement, $document, $$HashMap,
+ $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) {
+
+ var activeAnimationsLookup = new $$HashMap();
+ var disabledElementsLookup = new $$HashMap();
+ var animationsEnabled = null;
+
+ function postDigestTaskFactory() {
+ var postDigestCalled = false;
+ return function(fn) {
+ // we only issue a call to postDigest before
+ // it has first passed. This prevents any callbacks
+ // from not firing once the animation has completed
+ // since it will be out of the digest cycle.
+ if (postDigestCalled) {
+ fn();
+ } else {
+ $rootScope.$$postDigest(function() {
+ postDigestCalled = true;
+ fn();
+ });
+ }
+ };
+ }
+
+ // Wait until all directive and route-related templates are downloaded and
+ // compiled. The $templateRequest.totalPendingRequests variable keeps track of
+ // all of the remote templates being currently downloaded. If there are no
+ // templates currently downloading then the watcher will still fire anyway.
+ var deregisterWatch = $rootScope.$watch(
+ function() { return $templateRequest.totalPendingRequests === 0; },
+ function(isEmpty) {
+ if (!isEmpty) return;
+ deregisterWatch();
+
+ // Now that all templates have been downloaded, $animate will wait until
+ // the post digest queue is empty before enabling animations. By having two
+ // calls to $postDigest calls we can ensure that the flag is enabled at the
+ // very end of the post digest queue. Since all of the animations in $animate
+ // use $postDigest, it's important that the code below executes at the end.
+ // This basically means that the page is fully downloaded and compiled before
+ // any animations are triggered.
+ $rootScope.$$postDigest(function() {
+ $rootScope.$$postDigest(function() {
+ // we check for null directly in the event that the application already called
+ // .enabled() with whatever arguments that it provided it with
+ if (animationsEnabled === null) {
+ animationsEnabled = true;
+ }
+ });
+ });
+ }
+ );
+
+ var callbackRegistry = Object.create(null);
+
+ // remember that the classNameFilter is set during the provider/config
+ // stage therefore we can optimize here and setup a helper function
+ var classNameFilter = $animateProvider.classNameFilter();
+ var isAnimatableClassName = !classNameFilter
+ ? function() { return true; }
+ : function(className) {
+ return classNameFilter.test(className);
+ };
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ function normalizeAnimationDetails(element, animation) {
+ return mergeAnimationDetails(element, animation, {});
+ }
+
+ // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
+ var contains = window.Node.prototype.contains || /** @this */ function(arg) {
+ // eslint-disable-next-line no-bitwise
+ return this === arg || !!(this.compareDocumentPosition(arg) & 16);
+ };
+
+ function findCallbacks(parent, element, event) {
+ var targetNode = getDomNode(element);
+ var targetParentNode = getDomNode(parent);
+
+ var matches = [];
+ var entries = callbackRegistry[event];
+ if (entries) {
+ forEach(entries, function(entry) {
+ if (contains.call(entry.node, targetNode)) {
+ matches.push(entry.callback);
+ } else if (event === 'leave' && contains.call(entry.node, targetParentNode)) {
+ matches.push(entry.callback);
+ }
+ });
+ }
+
+ return matches;
+ }
+
+ function filterFromRegistry(list, matchContainer, matchCallback) {
+ var containerNode = extractElementNode(matchContainer);
+ return list.filter(function(entry) {
+ var isMatch = entry.node === containerNode &&
+ (!matchCallback || entry.callback === matchCallback);
+ return !isMatch;
+ });
+ }
+
+ function cleanupEventListeners(phase, element) {
+ if (phase === 'close' && !element[0].parentNode) {
+ // If the element is not attached to a parentNode, it has been removed by
+ // the domOperation, and we can safely remove the event callbacks
+ $animate.off(element);
+ }
+ }
+
+ var $animate = {
+ on: function(event, container, callback) {
+ var node = extractElementNode(container);
+ callbackRegistry[event] = callbackRegistry[event] || [];
+ callbackRegistry[event].push({
+ node: node,
+ callback: callback
+ });
+
+ // Remove the callback when the element is removed from the DOM
+ jqLite(container).on('$destroy', function() {
+ var animationDetails = activeAnimationsLookup.get(node);
+
+ if (!animationDetails) {
+ // If there's an animation ongoing, the callback calling code will remove
+ // the event listeners. If we'd remove here, the callbacks would be removed
+ // before the animation ends
+ $animate.off(event, container, callback);
+ }
+ });
+ },
+
+ off: function(event, container, callback) {
+ if (arguments.length === 1 && !isString(arguments[0])) {
+ container = arguments[0];
+ for (var eventType in callbackRegistry) {
+ callbackRegistry[eventType] = filterFromRegistry(callbackRegistry[eventType], container);
+ }
+
+ return;
+ }
+
+ var entries = callbackRegistry[event];
+ if (!entries) return;
+
+ callbackRegistry[event] = arguments.length === 1
+ ? null
+ : filterFromRegistry(entries, container, callback);
+ },
+
+ pin: function(element, parentElement) {
+ assertArg(isElement(element), 'element', 'not an element');
+ assertArg(isElement(parentElement), 'parentElement', 'not an element');
+ element.data(NG_ANIMATE_PIN_DATA, parentElement);
+ },
+
+ push: function(element, event, options, domOperation) {
+ options = options || {};
+ options.domOperation = domOperation;
+ return queueAnimation(element, event, options);
+ },
+
+ // this method has four signatures:
+ // () - global getter
+ // (bool) - global setter
+ // (element) - element getter
+ // (element, bool) - element setter
+ enabled: function(element, bool) {
+ var argCount = arguments.length;
+
+ if (argCount === 0) {
+ // () - Global getter
+ bool = !!animationsEnabled;
+ } else {
+ var hasElement = isElement(element);
+
+ if (!hasElement) {
+ // (bool) - Global setter
+ bool = animationsEnabled = !!element;
+ } else {
+ var node = getDomNode(element);
+
+ if (argCount === 1) {
+ // (element) - Element getter
+ bool = !disabledElementsLookup.get(node);
+ } else {
+ // (element, bool) - Element setter
+ disabledElementsLookup.put(node, !bool);
+ }
+ }
+ }
+
+ return bool;
+ }
+ };
+
+ return $animate;
+
+ function queueAnimation(element, event, initialOptions) {
+ // we always make a copy of the options since
+ // there should never be any side effects on
+ // the input data when running `$animateCss`.
+ var options = copy(initialOptions);
+
+ var node, parent;
+ element = stripCommentsFromElement(element);
+ if (element) {
+ node = getDomNode(element);
+ parent = element.parent();
+ }
+
+ options = prepareAnimationOptions(options);
+
+ // we create a fake runner with a working promise.
+ // These methods will become available after the digest has passed
+ var runner = new $$AnimateRunner();
+
+ // this is used to trigger callbacks in postDigest mode
+ var runInNextPostDigestOrNow = postDigestTaskFactory();
+
+ if (isArray(options.addClass)) {
+ options.addClass = options.addClass.join(' ');
+ }
+
+ if (options.addClass && !isString(options.addClass)) {
+ options.addClass = null;
+ }
+
+ if (isArray(options.removeClass)) {
+ options.removeClass = options.removeClass.join(' ');
+ }
+
+ if (options.removeClass && !isString(options.removeClass)) {
+ options.removeClass = null;
+ }
+
+ if (options.from && !isObject(options.from)) {
+ options.from = null;
+ }
+
+ if (options.to && !isObject(options.to)) {
+ options.to = null;
+ }
+
+ // there are situations where a directive issues an animation for
+ // a jqLite wrapper that contains only comment nodes... If this
+ // happens then there is no way we can perform an animation
+ if (!node) {
+ close();
+ return runner;
+ }
+
+ var className = [node.className, options.addClass, options.removeClass].join(' ');
+ if (!isAnimatableClassName(className)) {
+ close();
+ return runner;
+ }
+
+ var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
+
+ var documentHidden = $document[0].hidden;
+
+ // this is a hard disable of all animations for the application or on
+ // the element itself, therefore there is no need to continue further
+ // past this point if not enabled
+ // Animations are also disabled if the document is currently hidden (page is not visible
+ // to the user), because browsers slow down or do not flush calls to requestAnimationFrame
+ var skipAnimations = !animationsEnabled || documentHidden || disabledElementsLookup.get(node);
+ var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {};
+ var hasExistingAnimation = !!existingAnimation.state;
+
+ // there is no point in traversing the same collection of parent ancestors if a followup
+ // animation will be run on the same element that already did all that checking work
+ if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state !== PRE_DIGEST_STATE)) {
+ skipAnimations = !areAnimationsAllowed(element, parent, event);
+ }
+
+ if (skipAnimations) {
+ // Callbacks should fire even if the document is hidden (regression fix for issue #14120)
+ if (documentHidden) notifyProgress(runner, event, 'start');
+ close();
+ if (documentHidden) notifyProgress(runner, event, 'close');
+ return runner;
+ }
+
+ if (isStructural) {
+ closeChildAnimations(element);
+ }
+
+ var newAnimation = {
+ structural: isStructural,
+ element: element,
+ event: event,
+ addClass: options.addClass,
+ removeClass: options.removeClass,
+ close: close,
+ options: options,
+ runner: runner
+ };
+
+ if (hasExistingAnimation) {
+ var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation);
+ if (skipAnimationFlag) {
+ if (existingAnimation.state === RUNNING_STATE) {
+ close();
+ return runner;
+ } else {
+ mergeAnimationDetails(element, existingAnimation, newAnimation);
+ return existingAnimation.runner;
+ }
+ }
+ var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation);
+ if (cancelAnimationFlag) {
+ if (existingAnimation.state === RUNNING_STATE) {
+ // this will end the animation right away and it is safe
+ // to do so since the animation is already running and the
+ // runner callback code will run in async
+ existingAnimation.runner.end();
+ } else if (existingAnimation.structural) {
+ // this means that the animation is queued into a digest, but
+ // hasn't started yet. Therefore it is safe to run the close
+ // method which will call the runner methods in async.
+ existingAnimation.close();
+ } else {
+ // this will merge the new animation options into existing animation options
+ mergeAnimationDetails(element, existingAnimation, newAnimation);
+
+ return existingAnimation.runner;
+ }
+ } else {
+ // a joined animation means that this animation will take over the existing one
+ // so an example would involve a leave animation taking over an enter. Then when
+ // the postDigest kicks in the enter will be ignored.
+ var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation);
+ if (joinAnimationFlag) {
+ if (existingAnimation.state === RUNNING_STATE) {
+ normalizeAnimationDetails(element, newAnimation);
+ } else {
+ applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
+
+ event = newAnimation.event = existingAnimation.event;
+ options = mergeAnimationDetails(element, existingAnimation, newAnimation);
+
+ //we return the same runner since only the option values of this animation will
+ //be fed into the `existingAnimation`.
+ return existingAnimation.runner;
+ }
+ }
+ }
+ } else {
+ // normalization in this case means that it removes redundant CSS classes that
+ // already exist (addClass) or do not exist (removeClass) on the element
+ normalizeAnimationDetails(element, newAnimation);
+ }
+
+ // when the options are merged and cleaned up we may end up not having to do
+ // an animation at all, therefore we should check this before issuing a post
+ // digest callback. Structural animations will always run no matter what.
+ var isValidAnimation = newAnimation.structural;
+ if (!isValidAnimation) {
+ // animate (from/to) can be quickly checked first, otherwise we check if any classes are present
+ isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0)
+ || hasAnimationClasses(newAnimation);
+ }
+
+ if (!isValidAnimation) {
+ close();
+ clearElementAnimationState(element);
+ return runner;
+ }
+
+ // the counter keeps track of cancelled animations
+ var counter = (existingAnimation.counter || 0) + 1;
+ newAnimation.counter = counter;
+
+ markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation);
+
+ $rootScope.$$postDigest(function() {
+ var animationDetails = activeAnimationsLookup.get(node);
+ var animationCancelled = !animationDetails;
+ animationDetails = animationDetails || {};
+
+ // if addClass/removeClass is called before something like enter then the
+ // registered parent element may not be present. The code below will ensure
+ // that a final value for parent element is obtained
+ var parentElement = element.parent() || [];
+
+ // animate/structural/class-based animations all have requirements. Otherwise there
+ // is no point in performing an animation. The parent node must also be set.
+ var isValidAnimation = parentElement.length > 0
+ && (animationDetails.event === 'animate'
+ || animationDetails.structural
+ || hasAnimationClasses(animationDetails));
+
+ // this means that the previous animation was cancelled
+ // even if the follow-up animation is the same event
+ if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) {
+ // if another animation did not take over then we need
+ // to make sure that the domOperation and options are
+ // handled accordingly
+ if (animationCancelled) {
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+ }
+
+ // if the event changed from something like enter to leave then we do
+ // it, otherwise if it's the same then the end result will be the same too
+ if (animationCancelled || (isStructural && animationDetails.event !== event)) {
+ options.domOperation();
+ runner.end();
+ }
+
+ // in the event that the element animation was not cancelled or a follow-up animation
+ // isn't allowed to animate from here then we need to clear the state of the element
+ // so that any future animations won't read the expired animation data.
+ if (!isValidAnimation) {
+ clearElementAnimationState(element);
+ }
+
+ return;
+ }
+
+ // this combined multiple class to addClass / removeClass into a setClass event
+ // so long as a structural event did not take over the animation
+ event = !animationDetails.structural && hasAnimationClasses(animationDetails, true)
+ ? 'setClass'
+ : animationDetails.event;
+
+ markElementAnimationState(element, RUNNING_STATE);
+ var realRunner = $$animation(element, event, animationDetails.options);
+
+ // this will update the runner's flow-control events based on
+ // the `realRunner` object.
+ runner.setHost(realRunner);
+ notifyProgress(runner, event, 'start', {});
+
+ realRunner.done(function(status) {
+ close(!status);
+ var animationDetails = activeAnimationsLookup.get(node);
+ if (animationDetails && animationDetails.counter === counter) {
+ clearElementAnimationState(getDomNode(element));
+ }
+ notifyProgress(runner, event, 'close', {});
+ });
+ });
+
+ return runner;
+
+ function notifyProgress(runner, event, phase, data) {
+ runInNextPostDigestOrNow(function() {
+ var callbacks = findCallbacks(parent, element, event);
+ if (callbacks.length) {
+ // do not optimize this call here to RAF because
+ // we don't know how heavy the callback code here will
+ // be and if this code is buffered then this can
+ // lead to a performance regression.
+ $$rAF(function() {
+ forEach(callbacks, function(callback) {
+ callback(element, phase, data);
+ });
+ cleanupEventListeners(phase, element);
+ });
+ } else {
+ cleanupEventListeners(phase, element);
+ }
+ });
+ runner.progress(event, phase, data);
+ }
+
+ function close(reject) {
+ clearGeneratedClasses(element, options);
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+ options.domOperation();
+ runner.complete(!reject);
+ }
+ }
+
+ function closeChildAnimations(element) {
+ var node = getDomNode(element);
+ var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']');
+ forEach(children, function(child) {
+ var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME), 10);
+ var animationDetails = activeAnimationsLookup.get(child);
+ if (animationDetails) {
+ switch (state) {
+ case RUNNING_STATE:
+ animationDetails.runner.end();
+ /* falls through */
+ case PRE_DIGEST_STATE:
+ activeAnimationsLookup.remove(child);
+ break;
+ }
+ }
+ });
+ }
+
+ function clearElementAnimationState(element) {
+ var node = getDomNode(element);
+ node.removeAttribute(NG_ANIMATE_ATTR_NAME);
+ activeAnimationsLookup.remove(node);
+ }
+
+ function isMatchingElement(nodeOrElmA, nodeOrElmB) {
+ return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB);
+ }
+
+ /**
+ * This fn returns false if any of the following is true:
+ * a) animations on any parent element are disabled, and animations on the element aren't explicitly allowed
+ * b) a parent element has an ongoing structural animation, and animateChildren is false
+ * c) the element is not a child of the body
+ * d) the element is not a child of the $rootElement
+ */
+ function areAnimationsAllowed(element, parentElement, event) {
+ var bodyElement = jqLite($document[0].body);
+ var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML';
+ var rootElementDetected = isMatchingElement(element, $rootElement);
+ var parentAnimationDetected = false;
+ var animateChildren;
+ var elementDisabled = disabledElementsLookup.get(getDomNode(element));
+
+ var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA);
+ if (parentHost) {
+ parentElement = parentHost;
+ }
+
+ parentElement = getDomNode(parentElement);
+
+ while (parentElement) {
+ if (!rootElementDetected) {
+ // angular doesn't want to attempt to animate elements outside of the application
+ // therefore we need to ensure that the rootElement is an ancestor of the current element
+ rootElementDetected = isMatchingElement(parentElement, $rootElement);
+ }
+
+ if (parentElement.nodeType !== ELEMENT_NODE) {
+ // no point in inspecting the #document element
+ break;
+ }
+
+ var details = activeAnimationsLookup.get(parentElement) || {};
+ // either an enter, leave or move animation will commence
+ // therefore we can't allow any animations to take place
+ // but if a parent animation is class-based then that's ok
+ if (!parentAnimationDetected) {
+ var parentElementDisabled = disabledElementsLookup.get(parentElement);
+
+ if (parentElementDisabled === true && elementDisabled !== false) {
+ // disable animations if the user hasn't explicitly enabled animations on the
+ // current element
+ elementDisabled = true;
+ // element is disabled via parent element, no need to check anything else
+ break;
+ } else if (parentElementDisabled === false) {
+ elementDisabled = false;
+ }
+ parentAnimationDetected = details.structural;
+ }
+
+ if (isUndefined(animateChildren) || animateChildren === true) {
+ var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA);
+ if (isDefined(value)) {
+ animateChildren = value;
+ }
+ }
+
+ // there is no need to continue traversing at this point
+ if (parentAnimationDetected && animateChildren === false) break;
+
+ if (!bodyElementDetected) {
+ // we also need to ensure that the element is or will be a part of the body element
+ // otherwise it is pointless to even issue an animation to be rendered
+ bodyElementDetected = isMatchingElement(parentElement, bodyElement);
+ }
+
+ if (bodyElementDetected && rootElementDetected) {
+ // If both body and root have been found, any other checks are pointless,
+ // as no animation data should live outside the application
+ break;
+ }
+
+ if (!rootElementDetected) {
+ // If no rootElement is detected, check if the parentElement is pinned to another element
+ parentHost = jqLite.data(parentElement, NG_ANIMATE_PIN_DATA);
+ if (parentHost) {
+ // The pin target element becomes the next parent element
+ parentElement = getDomNode(parentHost);
+ continue;
+ }
+ }
+
+ parentElement = parentElement.parentNode;
+ }
+
+ var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true;
+ return allowAnimation && rootElementDetected && bodyElementDetected;
+ }
+
+ function markElementAnimationState(element, state, details) {
+ details = details || {};
+ details.state = state;
+
+ var node = getDomNode(element);
+ node.setAttribute(NG_ANIMATE_ATTR_NAME, state);
+
+ var oldValue = activeAnimationsLookup.get(node);
+ var newValue = oldValue
+ ? extend(oldValue, details)
+ : details;
+ activeAnimationsLookup.put(node, newValue);
+ }
+ }];
+}];
+
+/* exported $$AnimationProvider */
+
+var $$AnimationProvider = ['$animateProvider', /** @this */ function($animateProvider) {
+ var NG_ANIMATE_REF_ATTR = 'ng-animate-ref';
+
+ var drivers = this.drivers = [];
+
+ var RUNNER_STORAGE_KEY = '$$animationRunner';
+
+ function setRunner(element, runner) {
+ element.data(RUNNER_STORAGE_KEY, runner);
+ }
+
+ function removeRunner(element) {
+ element.removeData(RUNNER_STORAGE_KEY);
+ }
+
+ function getRunner(element) {
+ return element.data(RUNNER_STORAGE_KEY);
+ }
+
+ this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler',
+ function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) {
+
+ var animationQueue = [];
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ function sortAnimations(animations) {
+ var tree = { children: [] };
+ var i, lookup = new $$HashMap();
+
+ // this is done first beforehand so that the hashmap
+ // is filled with a list of the elements that will be animated
+ for (i = 0; i < animations.length; i++) {
+ var animation = animations[i];
+ lookup.put(animation.domNode, animations[i] = {
+ domNode: animation.domNode,
+ fn: animation.fn,
+ children: []
+ });
+ }
+
+ for (i = 0; i < animations.length; i++) {
+ processNode(animations[i]);
+ }
+
+ return flatten(tree);
+
+ function processNode(entry) {
+ if (entry.processed) return entry;
+ entry.processed = true;
+
+ var elementNode = entry.domNode;
+ var parentNode = elementNode.parentNode;
+ lookup.put(elementNode, entry);
+
+ var parentEntry;
+ while (parentNode) {
+ parentEntry = lookup.get(parentNode);
+ if (parentEntry) {
+ if (!parentEntry.processed) {
+ parentEntry = processNode(parentEntry);
+ }
+ break;
+ }
+ parentNode = parentNode.parentNode;
+ }
+
+ (parentEntry || tree).children.push(entry);
+ return entry;
+ }
+
+ function flatten(tree) {
+ var result = [];
+ var queue = [];
+ var i;
+
+ for (i = 0; i < tree.children.length; i++) {
+ queue.push(tree.children[i]);
+ }
+
+ var remainingLevelEntries = queue.length;
+ var nextLevelEntries = 0;
+ var row = [];
+
+ for (i = 0; i < queue.length; i++) {
+ var entry = queue[i];
+ if (remainingLevelEntries <= 0) {
+ remainingLevelEntries = nextLevelEntries;
+ nextLevelEntries = 0;
+ result.push(row);
+ row = [];
+ }
+ row.push(entry.fn);
+ entry.children.forEach(function(childEntry) {
+ nextLevelEntries++;
+ queue.push(childEntry);
+ });
+ remainingLevelEntries--;
+ }
+
+ if (row.length) {
+ result.push(row);
+ }
+
+ return result;
+ }
+ }
+
+ // TODO(matsko): document the signature in a better way
+ return function(element, event, options) {
+ options = prepareAnimationOptions(options);
+ var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
+
+ // there is no animation at the current moment, however
+ // these runner methods will get later updated with the
+ // methods leading into the driver's end/cancel methods
+ // for now they just stop the animation from starting
+ var runner = new $$AnimateRunner({
+ end: function() { close(); },
+ cancel: function() { close(true); }
+ });
+
+ if (!drivers.length) {
+ close();
+ return runner;
+ }
+
+ setRunner(element, runner);
+
+ var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass));
+ var tempClasses = options.tempClasses;
+ if (tempClasses) {
+ classes += ' ' + tempClasses;
+ options.tempClasses = null;
+ }
+
+ var prepareClassName;
+ if (isStructural) {
+ prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX;
+ $$jqLite.addClass(element, prepareClassName);
+ }
+
+ animationQueue.push({
+ // this data is used by the postDigest code and passed into
+ // the driver step function
+ element: element,
+ classes: classes,
+ event: event,
+ structural: isStructural,
+ options: options,
+ beforeStart: beforeStart,
+ close: close
+ });
+
+ element.on('$destroy', handleDestroyedElement);
+
+ // we only want there to be one function called within the post digest
+ // block. This way we can group animations for all the animations that
+ // were apart of the same postDigest flush call.
+ if (animationQueue.length > 1) return runner;
+
+ $rootScope.$$postDigest(function() {
+ var animations = [];
+ forEach(animationQueue, function(entry) {
+ // the element was destroyed early on which removed the runner
+ // form its storage. This means we can't animate this element
+ // at all and it already has been closed due to destruction.
+ if (getRunner(entry.element)) {
+ animations.push(entry);
+ } else {
+ entry.close();
+ }
+ });
+
+ // now any future animations will be in another postDigest
+ animationQueue.length = 0;
+
+ var groupedAnimations = groupAnimations(animations);
+ var toBeSortedAnimations = [];
+
+ forEach(groupedAnimations, function(animationEntry) {
+ toBeSortedAnimations.push({
+ domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element),
+ fn: function triggerAnimationStart() {
+ // it's important that we apply the `ng-animate` CSS class and the
+ // temporary classes before we do any driver invoking since these
+ // CSS classes may be required for proper CSS detection.
+ animationEntry.beforeStart();
+
+ var startAnimationFn, closeFn = animationEntry.close;
+
+ // in the event that the element was removed before the digest runs or
+ // during the RAF sequencing then we should not trigger the animation.
+ var targetElement = animationEntry.anchors
+ ? (animationEntry.from.element || animationEntry.to.element)
+ : animationEntry.element;
+
+ if (getRunner(targetElement)) {
+ var operation = invokeFirstDriver(animationEntry);
+ if (operation) {
+ startAnimationFn = operation.start;
+ }
+ }
+
+ if (!startAnimationFn) {
+ closeFn();
+ } else {
+ var animationRunner = startAnimationFn();
+ animationRunner.done(function(status) {
+ closeFn(!status);
+ });
+ updateAnimationRunners(animationEntry, animationRunner);
+ }
+ }
+ });
+ });
+
+ // we need to sort each of the animations in order of parent to child
+ // relationships. This ensures that the child classes are applied at the
+ // right time.
+ $$rAFScheduler(sortAnimations(toBeSortedAnimations));
+ });
+
+ return runner;
+
+ // TODO(matsko): change to reference nodes
+ function getAnchorNodes(node) {
+ var SELECTOR = '[' + NG_ANIMATE_REF_ATTR + ']';
+ var items = node.hasAttribute(NG_ANIMATE_REF_ATTR)
+ ? [node]
+ : node.querySelectorAll(SELECTOR);
+ var anchors = [];
+ forEach(items, function(node) {
+ var attr = node.getAttribute(NG_ANIMATE_REF_ATTR);
+ if (attr && attr.length) {
+ anchors.push(node);
+ }
+ });
+ return anchors;
+ }
+
+ function groupAnimations(animations) {
+ var preparedAnimations = [];
+ var refLookup = {};
+ forEach(animations, function(animation, index) {
+ var element = animation.element;
+ var node = getDomNode(element);
+ var event = animation.event;
+ var enterOrMove = ['enter', 'move'].indexOf(event) >= 0;
+ var anchorNodes = animation.structural ? getAnchorNodes(node) : [];
+
+ if (anchorNodes.length) {
+ var direction = enterOrMove ? 'to' : 'from';
+
+ forEach(anchorNodes, function(anchor) {
+ var key = anchor.getAttribute(NG_ANIMATE_REF_ATTR);
+ refLookup[key] = refLookup[key] || {};
+ refLookup[key][direction] = {
+ animationID: index,
+ element: jqLite(anchor)
+ };
+ });
+ } else {
+ preparedAnimations.push(animation);
+ }
+ });
+
+ var usedIndicesLookup = {};
+ var anchorGroups = {};
+ forEach(refLookup, function(operations, key) {
+ var from = operations.from;
+ var to = operations.to;
+
+ if (!from || !to) {
+ // only one of these is set therefore we can't have an
+ // anchor animation since all three pieces are required
+ var index = from ? from.animationID : to.animationID;
+ var indexKey = index.toString();
+ if (!usedIndicesLookup[indexKey]) {
+ usedIndicesLookup[indexKey] = true;
+ preparedAnimations.push(animations[index]);
+ }
+ return;
+ }
+
+ var fromAnimation = animations[from.animationID];
+ var toAnimation = animations[to.animationID];
+ var lookupKey = from.animationID.toString();
+ if (!anchorGroups[lookupKey]) {
+ var group = anchorGroups[lookupKey] = {
+ structural: true,
+ beforeStart: function() {
+ fromAnimation.beforeStart();
+ toAnimation.beforeStart();
+ },
+ close: function() {
+ fromAnimation.close();
+ toAnimation.close();
+ },
+ classes: cssClassesIntersection(fromAnimation.classes, toAnimation.classes),
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [] // TODO(matsko): change to reference nodes
+ };
+
+ // the anchor animations require that the from and to elements both have at least
+ // one shared CSS class which effectively marries the two elements together to use
+ // the same animation driver and to properly sequence the anchor animation.
+ if (group.classes.length) {
+ preparedAnimations.push(group);
+ } else {
+ preparedAnimations.push(fromAnimation);
+ preparedAnimations.push(toAnimation);
+ }
+ }
+
+ anchorGroups[lookupKey].anchors.push({
+ 'out': from.element, 'in': to.element
+ });
+ });
+
+ return preparedAnimations;
+ }
+
+ function cssClassesIntersection(a,b) {
+ a = a.split(' ');
+ b = b.split(' ');
+ var matches = [];
+
+ for (var i = 0; i < a.length; i++) {
+ var aa = a[i];
+ if (aa.substring(0,3) === 'ng-') continue;
+
+ for (var j = 0; j < b.length; j++) {
+ if (aa === b[j]) {
+ matches.push(aa);
+ break;
+ }
+ }
+ }
+
+ return matches.join(' ');
+ }
+
+ function invokeFirstDriver(animationDetails) {
+ // we loop in reverse order since the more general drivers (like CSS and JS)
+ // may attempt more elements, but custom drivers are more particular
+ for (var i = drivers.length - 1; i >= 0; i--) {
+ var driverName = drivers[i];
+ var factory = $injector.get(driverName);
+ var driver = factory(animationDetails);
+ if (driver) {
+ return driver;
+ }
+ }
+ }
+
+ function beforeStart() {
+ element.addClass(NG_ANIMATE_CLASSNAME);
+ if (tempClasses) {
+ $$jqLite.addClass(element, tempClasses);
+ }
+ if (prepareClassName) {
+ $$jqLite.removeClass(element, prepareClassName);
+ prepareClassName = null;
+ }
+ }
+
+ function updateAnimationRunners(animation, newRunner) {
+ if (animation.from && animation.to) {
+ update(animation.from.element);
+ update(animation.to.element);
+ } else {
+ update(animation.element);
+ }
+
+ function update(element) {
+ var runner = getRunner(element);
+ if (runner) runner.setHost(newRunner);
+ }
+ }
+
+ function handleDestroyedElement() {
+ var runner = getRunner(element);
+ if (runner && (event !== 'leave' || !options.$$domOperationFired)) {
+ runner.end();
+ }
+ }
+
+ function close(rejected) {
+ element.off('$destroy', handleDestroyedElement);
+ removeRunner(element);
+
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+ options.domOperation();
+
+ if (tempClasses) {
+ $$jqLite.removeClass(element, tempClasses);
+ }
+
+ element.removeClass(NG_ANIMATE_CLASSNAME);
+ runner.complete(!rejected);
+ }
+ };
+ }];
+}];
+
+/**
+ * @ngdoc directive
+ * @name ngAnimateSwap
+ * @restrict A
+ * @scope
+ *
+ * @description
+ *
+ * ngAnimateSwap is a animation-oriented directive that allows for the container to
+ * be removed and entered in whenever the associated expression changes. A
+ * common usecase for this directive is a rotating banner or slider component which
+ * contains one image being present at a time. When the active image changes
+ * then the old image will perform a `leave` animation and the new element
+ * will be inserted via an `enter` animation.
+ *
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|--------------------------------------|
+ * | {@link ng.$animate#enter enter} | when the new element is inserted to the DOM |
+ * | {@link ng.$animate#leave leave} | when the old element is removed from the DOM |
+ *
+ * @example
+ *
+ *
+ *
+ *
+ * {{ number }}
+ *
+ *
+ *
+ *
+ * angular.module('ngAnimateSwapExample', ['ngAnimate'])
+ * .controller('AppCtrl', ['$scope', '$interval', function($scope, $interval) {
+ * $scope.number = 0;
+ * $interval(function() {
+ * $scope.number++;
+ * }, 1000);
+ *
+ * var colors = ['red','blue','green','yellow','orange'];
+ * $scope.colorClass = function(number) {
+ * return colors[number % colors.length];
+ * };
+ * }]);
+ *
+ *
+ * .container {
+ * height:250px;
+ * width:250px;
+ * position:relative;
+ * overflow:hidden;
+ * border:2px solid black;
+ * }
+ * .container .cell {
+ * font-size:150px;
+ * text-align:center;
+ * line-height:250px;
+ * position:absolute;
+ * top:0;
+ * left:0;
+ * right:0;
+ * border-bottom:2px solid black;
+ * }
+ * .swap-animation.ng-enter, .swap-animation.ng-leave {
+ * transition:0.5s linear all;
+ * }
+ * .swap-animation.ng-enter {
+ * top:-250px;
+ * }
+ * .swap-animation.ng-enter-active {
+ * top:0px;
+ * }
+ * .swap-animation.ng-leave {
+ * top:0px;
+ * }
+ * .swap-animation.ng-leave-active {
+ * top:250px;
+ * }
+ * .red { background:red; }
+ * .green { background:green; }
+ * .blue { background:blue; }
+ * .yellow { background:yellow; }
+ * .orange { background:orange; }
+ *
+ *
+ */
+var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $rootScope) {
+ return {
+ restrict: 'A',
+ transclude: 'element',
+ terminal: true,
+ priority: 600, // we use 600 here to ensure that the directive is caught before others
+ link: function(scope, $element, attrs, ctrl, $transclude) {
+ var previousElement, previousScope;
+ scope.$watchCollection(attrs.ngAnimateSwap || attrs['for'], function(value) {
+ if (previousElement) {
+ $animate.leave(previousElement);
+ }
+ if (previousScope) {
+ previousScope.$destroy();
+ previousScope = null;
+ }
+ if (value || value === 0) {
+ previousScope = scope.$new();
+ $transclude(previousScope, function(element) {
+ previousElement = element;
+ $animate.enter(element, null, $element);
+ });
+ }
+ });
+ }
+ };
+}];
+
+/**
+ * @ngdoc module
+ * @name ngAnimate
+ * @description
+ *
+ * The `ngAnimate` module provides support for CSS-based animations (keyframes and transitions) as well as JavaScript-based animations via
+ * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an Angular app.
+ *
+ *
+ *
+ * # Usage
+ * Simply put, there are two ways to make use of animations when ngAnimate is used: by using **CSS** and **JavaScript**. The former works purely based
+ * using CSS (by using matching CSS selectors/styles) and the latter triggers animations that are registered via `module.animation()`. For
+ * both CSS and JS animations the sole requirement is to have a matching `CSS class` that exists both in the registered animation and within
+ * the HTML element that the animation will be triggered on.
+ *
+ * ## Directive Support
+ * The following directives are "animation aware":
+ *
+ * | Directive | Supported Animations |
+ * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
+ * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
+ * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
+ * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
+ * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
+ * | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
+ * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) |
+ * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) |
+ * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
+ * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) |
+ * | {@link module:ngMessages#animations ngMessage} | enter and leave |
+ *
+ * (More information can be found by visiting each the documentation associated with each directive.)
+ *
+ * ## CSS-based Animations
+ *
+ * CSS-based animations with ngAnimate are unique since they require no JavaScript code at all. By using a CSS class that we reference between our HTML
+ * and CSS code we can create an animation that will be picked up by Angular when an underlying directive performs an operation.
+ *
+ * The example below shows how an `enter` animation can be made possible on an element using `ng-if`:
+ *
+ * ```html
+ *
+ * Fade me in out
+ *
+ *
+ *
+ * ```
+ *
+ * Notice the CSS class **fade**? We can now create the CSS transition code that references this class:
+ *
+ * ```css
+ * /* The starting CSS styles for the enter animation */
+ * .fade.ng-enter {
+ * transition:0.5s linear all;
+ * opacity:0;
+ * }
+ *
+ * /* The finishing CSS styles for the enter animation */
+ * .fade.ng-enter.ng-enter-active {
+ * opacity:1;
+ * }
+ * ```
+ *
+ * The key thing to remember here is that, depending on the animation event (which each of the directives above trigger depending on what's going on) two
+ * generated CSS classes will be applied to the element; in the example above we have `.ng-enter` and `.ng-enter-active`. For CSS transitions, the transition
+ * code **must** be defined within the starting CSS class (in this case `.ng-enter`). The destination class is what the transition will animate towards.
+ *
+ * If for example we wanted to create animations for `leave` and `move` (ngRepeat triggers move) then we can do so using the same CSS naming conventions:
+ *
+ * ```css
+ * /* now the element will fade out before it is removed from the DOM */
+ * .fade.ng-leave {
+ * transition:0.5s linear all;
+ * opacity:1;
+ * }
+ * .fade.ng-leave.ng-leave-active {
+ * opacity:0;
+ * }
+ * ```
+ *
+ * We can also make use of **CSS Keyframes** by referencing the keyframe animation within the starting CSS class:
+ *
+ * ```css
+ * /* there is no need to define anything inside of the destination
+ * CSS class since the keyframe will take charge of the animation */
+ * .fade.ng-leave {
+ * animation: my_fade_animation 0.5s linear;
+ * -webkit-animation: my_fade_animation 0.5s linear;
+ * }
+ *
+ * @keyframes my_fade_animation {
+ * from { opacity:1; }
+ * to { opacity:0; }
+ * }
+ *
+ * @-webkit-keyframes my_fade_animation {
+ * from { opacity:1; }
+ * to { opacity:0; }
+ * }
+ * ```
+ *
+ * Feel free also mix transitions and keyframes together as well as any other CSS classes on the same element.
+ *
+ * ### CSS Class-based Animations
+ *
+ * Class-based animations (animations that are triggered via `ngClass`, `ngShow`, `ngHide` and some other directives) have a slightly different
+ * naming convention. Class-based animations are basic enough that a standard transition or keyframe can be referenced on the class being added
+ * and removed.
+ *
+ * For example if we wanted to do a CSS animation for `ngHide` then we place an animation on the `.ng-hide` CSS class:
+ *
+ * ```html
+ *
+ * Show and hide me
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * All that is going on here with ngShow/ngHide behind the scenes is the `.ng-hide` class is added/removed (when the hidden state is valid). Since
+ * ngShow and ngHide are animation aware then we can match up a transition and ngAnimate handles the rest.
+ *
+ * In addition the addition and removal of the CSS class, ngAnimate also provides two helper methods that we can use to further decorate the animation
+ * with CSS styles.
+ *
+ * ```html
+ *
+ * Highlight this box
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * We can also make use of CSS keyframes by placing them within the CSS classes.
+ *
+ *
+ * ### CSS Staggering Animations
+ * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a
+ * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be
+ * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for
+ * the animation. The style property expected within the stagger class can either be a **transition-delay** or an
+ * **animation-delay** property (or both if your animation contains both transitions and keyframe animations).
+ *
+ * ```css
+ * .my-animation.ng-enter {
+ * /* standard transition code */
+ * transition: 1s linear all;
+ * opacity:0;
+ * }
+ * .my-animation.ng-enter-stagger {
+ * /* this will have a 100ms delay between each successive leave animation */
+ * transition-delay: 0.1s;
+ *
+ * /* As of 1.4.4, this must always be set: it signals ngAnimate
+ * to not accidentally inherit a delay property from another CSS class */
+ * transition-duration: 0s;
+ * }
+ * .my-animation.ng-enter.ng-enter-active {
+ * /* standard transition styles */
+ * opacity:1;
+ * }
+ * ```
+ *
+ * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations
+ * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this
+ * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation
+ * will also be reset if one or more animation frames have passed since the multiple calls to `$animate` were fired.
+ *
+ * The following code will issue the **ng-leave-stagger** event on the element provided:
+ *
+ * ```js
+ * var kids = parent.children();
+ *
+ * $animate.leave(kids[0]); //stagger index=0
+ * $animate.leave(kids[1]); //stagger index=1
+ * $animate.leave(kids[2]); //stagger index=2
+ * $animate.leave(kids[3]); //stagger index=3
+ * $animate.leave(kids[4]); //stagger index=4
+ *
+ * window.requestAnimationFrame(function() {
+ * //stagger has reset itself
+ * $animate.leave(kids[5]); //stagger index=0
+ * $animate.leave(kids[6]); //stagger index=1
+ *
+ * $scope.$digest();
+ * });
+ * ```
+ *
+ * Stagger animations are currently only supported within CSS-defined animations.
+ *
+ * ### The `ng-animate` CSS class
+ *
+ * When ngAnimate is animating an element it will apply the `ng-animate` CSS class to the element for the duration of the animation.
+ * This is a temporary CSS class and it will be removed once the animation is over (for both JavaScript and CSS-based animations).
+ *
+ * Therefore, animations can be applied to an element using this temporary class directly via CSS.
+ *
+ * ```css
+ * .zipper.ng-animate {
+ * transition:0.5s linear all;
+ * }
+ * .zipper.ng-enter {
+ * opacity:0;
+ * }
+ * .zipper.ng-enter.ng-enter-active {
+ * opacity:1;
+ * }
+ * .zipper.ng-leave {
+ * opacity:1;
+ * }
+ * .zipper.ng-leave.ng-leave-active {
+ * opacity:0;
+ * }
+ * ```
+ *
+ * (Note that the `ng-animate` CSS class is reserved and it cannot be applied on an element directly since ngAnimate will always remove
+ * the CSS class once an animation has completed.)
+ *
+ *
+ * ### The `ng-[event]-prepare` class
+ *
+ * This is a special class that can be used to prevent unwanted flickering / flash of content before
+ * the actual animation starts. The class is added as soon as an animation is initialized, but removed
+ * before the actual animation starts (after waiting for a $digest).
+ * It is also only added for *structural* animations (`enter`, `move`, and `leave`).
+ *
+ * In practice, flickering can appear when nesting elements with structural animations such as `ngIf`
+ * into elements that have class-based animations such as `ngClass`.
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating.
+ * In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts:
+ *
+ * ```css
+ * .message.ng-enter-prepare {
+ * opacity: 0;
+ * }
+ *
+ * ```
+ *
+ * ## JavaScript-based Animations
+ *
+ * ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared
+ * CSS class that is referenced in our HTML code) but in addition we need to register the JavaScript animation on the module. By making use of the
+ * `module.animation()` module function we can register the animation.
+ *
+ * Let's see an example of a enter/leave animation using `ngRepeat`:
+ *
+ * ```html
+ *
+ * {{ item }}
+ *
+ * ```
+ *
+ * See the **slide** CSS class? Let's use that class to define an animation that we'll structure in our module code by using `module.animation`:
+ *
+ * ```js
+ * myModule.animation('.slide', [function() {
+ * return {
+ * // make note that other events (like addClass/removeClass)
+ * // have different function input parameters
+ * enter: function(element, doneFn) {
+ * jQuery(element).fadeIn(1000, doneFn);
+ *
+ * // remember to call doneFn so that angular
+ * // knows that the animation has concluded
+ * },
+ *
+ * move: function(element, doneFn) {
+ * jQuery(element).fadeIn(1000, doneFn);
+ * },
+ *
+ * leave: function(element, doneFn) {
+ * jQuery(element).fadeOut(1000, doneFn);
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as
+ * greensock.js and velocity.js.
+ *
+ * If our animation code class-based (meaning that something like `ngClass`, `ngHide` and `ngShow` triggers it) then we can still define
+ * our animations inside of the same registered animation, however, the function input arguments are a bit different:
+ *
+ * ```html
+ *
+ * this box is moody
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * ```js
+ * myModule.animation('.colorful', [function() {
+ * return {
+ * addClass: function(element, className, doneFn) {
+ * // do some cool animation and call the doneFn
+ * },
+ * removeClass: function(element, className, doneFn) {
+ * // do some cool animation and call the doneFn
+ * },
+ * setClass: function(element, addedClass, removedClass, doneFn) {
+ * // do some cool animation and call the doneFn
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * ## CSS + JS Animations Together
+ *
+ * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of Angular,
+ * defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore the example below will only result in **JS animations taking
+ * charge of the animation**:
+ *
+ * ```html
+ *
+ * Slide in and out
+ *
+ * ```
+ *
+ * ```js
+ * myModule.animation('.slide', [function() {
+ * return {
+ * enter: function(element, doneFn) {
+ * jQuery(element).slideIn(1000, doneFn);
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * ```css
+ * .slide.ng-enter {
+ * transition:0.5s linear all;
+ * transform:translateY(-100px);
+ * }
+ * .slide.ng-enter.ng-enter-active {
+ * transform:translateY(0);
+ * }
+ * ```
+ *
+ * Does this mean that CSS and JS animations cannot be used together? Do JS-based animations always have higher priority? We can make up for the
+ * lack of CSS animations by using the `$animateCss` service to trigger our own tweaked-out, CSS-based animations directly from
+ * our own JS-based animation code:
+ *
+ * ```js
+ * myModule.animation('.slide', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element) {
+* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`.
+ * return $animateCss(element, {
+ * event: 'enter',
+ * structural: true
+ * });
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework.
+ *
+ * The `$animateCss` service is very powerful since we can feed in all kinds of extra properties that will be evaluated and fed into a CSS transition or
+ * keyframe animation. For example if we wanted to animate the height of an element while adding and removing classes then we can do so by providing that
+ * data into `$animateCss` directly:
+ *
+ * ```js
+ * myModule.animation('.slide', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element) {
+ * return $animateCss(element, {
+ * event: 'enter',
+ * structural: true,
+ * addClass: 'maroon-setting',
+ * from: { height:0 },
+ * to: { height: 200 }
+ * });
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * Now we can fill in the rest via our transition CSS code:
+ *
+ * ```css
+ * /* the transition tells ngAnimate to make the animation happen */
+ * .slide.ng-enter { transition:0.5s linear all; }
+ *
+ * /* this extra CSS class will be absorbed into the transition
+ * since the $animateCss code is adding the class */
+ * .maroon-setting { background:red; }
+ * ```
+ *
+ * And `$animateCss` will figure out the rest. Just make sure to have the `done()` callback fire the `doneFn` function to signal when the animation is over.
+ *
+ * To learn more about what's possible be sure to visit the {@link ngAnimate.$animateCss $animateCss service}.
+ *
+ * ## Animation Anchoring (via `ng-animate-ref`)
+ *
+ * ngAnimate in AngularJS 1.4 comes packed with the ability to cross-animate elements between
+ * structural areas of an application (like views) by pairing up elements using an attribute
+ * called `ng-animate-ref`.
+ *
+ * Let's say for example we have two views that are managed by `ng-view` and we want to show
+ * that there is a relationship between two components situated in within these views. By using the
+ * `ng-animate-ref` attribute we can identify that the two components are paired together and we
+ * can then attach an animation, which is triggered when the view changes.
+ *
+ * Say for example we have the following template code:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Now, when the view changes (once the link is clicked), ngAnimate will examine the
+ * HTML contents to see if there is a match reference between any components in the view
+ * that is leaving and the view that is entering. It will scan both the view which is being
+ * removed (leave) and inserted (enter) to see if there are any paired DOM elements that
+ * contain a matching ref value.
+ *
+ * The two images match since they share the same ref value. ngAnimate will now create a
+ * transport element (which is a clone of the first image element) and it will then attempt
+ * to animate to the position of the second image element in the next view. For the animation to
+ * work a special CSS class called `ng-anchor` will be added to the transported element.
+ *
+ * We can now attach a transition onto the `.banner.ng-anchor` CSS class and then
+ * ngAnimate will handle the entire transition for us as well as the addition and removal of
+ * any changes of CSS classes between the elements:
+ *
+ * ```css
+ * .banner.ng-anchor {
+ * /* this animation will last for 1 second since there are
+ * two phases to the animation (an `in` and an `out` phase) */
+ * transition:0.5s linear all;
+ * }
+ * ```
+ *
+ * We also **must** include animations for the views that are being entered and removed
+ * (otherwise anchoring wouldn't be possible since the new view would be inserted right away).
+ *
+ * ```css
+ * .view-animation.ng-enter, .view-animation.ng-leave {
+ * transition:0.5s linear all;
+ * position:fixed;
+ * left:0;
+ * top:0;
+ * width:100%;
+ * }
+ * .view-animation.ng-enter {
+ * transform:translateX(100%);
+ * }
+ * .view-animation.ng-leave,
+ * .view-animation.ng-enter.ng-enter-active {
+ * transform:translateX(0%);
+ * }
+ * .view-animation.ng-leave.ng-leave-active {
+ * transform:translateX(-100%);
+ * }
+ * ```
+ *
+ * Now we can jump back to the anchor animation. When the animation happens, there are two stages that occur:
+ * an `out` and an `in` stage. The `out` stage happens first and that is when the element is animated away
+ * from its origin. Once that animation is over then the `in` stage occurs which animates the
+ * element to its destination. The reason why there are two animations is to give enough time
+ * for the enter animation on the new element to be ready.
+ *
+ * The example above sets up a transition for both the in and out phases, but we can also target the out or
+ * in phases directly via `ng-anchor-out` and `ng-anchor-in`.
+ *
+ * ```css
+ * .banner.ng-anchor-out {
+ * transition: 0.5s linear all;
+ *
+ * /* the scale will be applied during the out animation,
+ * but will be animated away when the in animation runs */
+ * transform: scale(1.2);
+ * }
+ *
+ * .banner.ng-anchor-in {
+ * transition: 1s linear all;
+ * }
+ * ```
+ *
+ *
+ *
+ *
+ * ### Anchoring Demo
+ *
+
+
+ Home
+
+
+
+
+ .record {
+ display:block;
+ font-size:20px;
+ }
+ .profile {
+ background:black;
+ color:white;
+ font-size:100px;
+ }
+ .view-container {
+ position:relative;
+ }
+ .view-container > .view.ng-animate {
+ position:absolute;
+ top:0;
+ left:0;
+ width:100%;
+ min-height:500px;
+ }
+ .view.ng-enter, .view.ng-leave,
+ .record.ng-anchor {
+ transition:0.5s linear all;
+ }
+ .view.ng-enter {
+ transform:translateX(100%);
+ }
+ .view.ng-enter.ng-enter-active, .view.ng-leave {
+ transform:translateX(0%);
+ }
+ .view.ng-leave.ng-leave-active {
+ transform:translateX(-100%);
+ }
+ .record.ng-anchor-out {
+ background:red;
+ }
+
+
+ *
+ * ### How is the element transported?
+ *
+ * When an anchor animation occurs, ngAnimate will clone the starting element and position it exactly where the starting
+ * element is located on screen via absolute positioning. The cloned element will be placed inside of the root element
+ * of the application (where ng-app was defined) and all of the CSS classes of the starting element will be applied. The
+ * element will then animate into the `out` and `in` animations and will eventually reach the coordinates and match
+ * the dimensions of the destination element. During the entire animation a CSS class of `.ng-animate-shim` will be applied
+ * to both the starting and destination elements in order to hide them from being visible (the CSS styling for the class
+ * is: `visibility:hidden`). Once the anchor reaches its destination then it will be removed and the destination element
+ * will become visible since the shim class will be removed.
+ *
+ * ### How is the morphing handled?
+ *
+ * CSS Anchoring relies on transitions and keyframes and the internal code is intelligent enough to figure out
+ * what CSS classes differ between the starting element and the destination element. These different CSS classes
+ * will be added/removed on the anchor element and a transition will be applied (the transition that is provided
+ * in the anchor class). Long story short, ngAnimate will figure out what classes to add and remove which will
+ * make the transition of the element as smooth and automatic as possible. Be sure to use simple CSS classes that
+ * do not rely on DOM nesting structure so that the anchor element appears the same as the starting element (since
+ * the cloned element is placed inside of root element which is likely close to the body element).
+ *
+ * Note that if the root element is on the `` element then the cloned node will be placed inside of body.
+ *
+ *
+ * ## Using $animate in your directive code
+ *
+ * So far we've explored how to feed in animations into an Angular application, but how do we trigger animations within our own directives in our application?
+ * By injecting the `$animate` service into our directive code, we can trigger structural and class-based hooks which can then be consumed by animations. Let's
+ * imagine we have a greeting box that shows and hides itself when the data changes
+ *
+ * ```html
+ * Hi there
+ * ```
+ *
+ * ```js
+ * ngModule.directive('greetingBox', ['$animate', function($animate) {
+ * return function(scope, element, attrs) {
+ * attrs.$observe('active', function(value) {
+ * value ? $animate.addClass(element, 'on') : $animate.removeClass(element, 'on');
+ * });
+ * });
+ * }]);
+ * ```
+ *
+ * Now the `on` CSS class is added and removed on the greeting box component. Now if we add a CSS class on top of the greeting box element
+ * in our HTML code then we can trigger a CSS or JS animation to happen.
+ *
+ * ```css
+ * /* normally we would create a CSS class to reference on the element */
+ * greeting-box.on { transition:0.5s linear all; background:green; color:white; }
+ * ```
+ *
+ * The `$animate` service contains a variety of other methods like `enter`, `leave`, `animate` and `setClass`. To learn more about what's
+ * possible be sure to visit the {@link ng.$animate $animate service API page}.
+ *
+ *
+ * ## Callbacks and Promises
+ *
+ * When `$animate` is called it returns a promise that can be used to capture when the animation has ended. Therefore if we were to trigger
+ * an animation (within our directive code) then we can continue performing directive and scope related activities after the animation has
+ * ended by chaining onto the returned promise that animation method returns.
+ *
+ * ```js
+ * // somewhere within the depths of the directive
+ * $animate.enter(element, parent).then(function() {
+ * //the animation has completed
+ * });
+ * ```
+ *
+ * (Note that earlier versions of Angular prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case
+ * anymore.)
+ *
+ * In addition to the animation promise, we can also make use of animation-related callbacks within our directives and controller code by registering
+ * an event listener using the `$animate` service. Let's say for example that an animation was triggered on our view
+ * routing controller to hook into that:
+ *
+ * ```js
+ * ngModule.controller('HomePageController', ['$animate', function($animate) {
+ * $animate.on('enter', ngViewElement, function(element) {
+ * // the animation for this route has completed
+ * }]);
+ * }])
+ * ```
+ *
+ * (Note that you will need to trigger a digest within the callback to get angular to notice any scope-related changes.)
+ */
+
+var copy;
+var extend;
+var forEach;
+var isArray;
+var isDefined;
+var isElement;
+var isFunction;
+var isObject;
+var isString;
+var isUndefined;
+var jqLite;
+var noop;
+
+/**
+ * @ngdoc service
+ * @name $animate
+ * @kind object
+ *
+ * @description
+ * The ngAnimate `$animate` service documentation is the same for the core `$animate` service.
+ *
+ * Click here {@link ng.$animate to learn more about animations with `$animate`}.
+ */
+angular.module('ngAnimate', [], function initAngularHelpers() {
+ // Access helpers from angular core.
+ // Do it inside a `config` block to ensure `window.angular` is available.
+ noop = angular.noop;
+ copy = angular.copy;
+ extend = angular.extend;
+ jqLite = angular.element;
+ forEach = angular.forEach;
+ isArray = angular.isArray;
+ isString = angular.isString;
+ isObject = angular.isObject;
+ isUndefined = angular.isUndefined;
+ isDefined = angular.isDefined;
+ isFunction = angular.isFunction;
+ isElement = angular.isElement;
+})
+ .directive('ngAnimateSwap', ngAnimateSwapDirective)
+
+ .directive('ngAnimateChildren', $$AnimateChildrenDirective)
+ .factory('$$rAFScheduler', $$rAFSchedulerFactory)
+
+ .provider('$$animateQueue', $$AnimateQueueProvider)
+ .provider('$$animation', $$AnimationProvider)
+
+ .provider('$animateCss', $AnimateCssProvider)
+ .provider('$$animateCssDriver', $$AnimateCssDriverProvider)
+
+ .provider('$$animateJs', $$AnimateJsProvider)
+ .provider('$$animateJsDriver', $$AnimateJsDriverProvider);
+
+
+})(window, window.angular);
diff --git a/dist/docs/grunt-scripts/angular-bootstrap-prettify.js b/dist/docs/grunt-scripts/angular-bootstrap-prettify.js
new file mode 100644
index 000000000..df0507e10
--- /dev/null
+++ b/dist/docs/grunt-scripts/angular-bootstrap-prettify.js
@@ -0,0 +1,323 @@
+'use strict';
+
+var directive = {};
+var service = { value: {} };
+
+var DEPENDENCIES = {
+ 'angular.js': 'http://code.angularjs.org/' + angular.version.full + '/angular.min.js',
+ 'angular-resource.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-resource.min.js',
+ 'angular-route.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-route.min.js',
+ 'angular-animate.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-animate.min.js',
+ 'angular-sanitize.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-sanitize.min.js',
+ 'angular-cookies.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-cookies.min.js'
+};
+
+
+function escape(text) {
+ return text.
+ replace(/\&/g, '&').
+ replace(/\/g, '>').
+ replace(/"/g, '"');
+}
+
+/**
+ * http://stackoverflow.com/questions/451486/pre-tag-loses-line-breaks-when-setting-innerhtml-in-ie
+ * http://stackoverflow.com/questions/195363/inserting-a-newline-into-a-pre-tag-ie-javascript
+ */
+function setHtmlIe8SafeWay(element, html) {
+ var newElement = angular.element('
\n';
+
+ setHtmlIe8SafeWay(element,
+ '
+
+
+ angular.module( 'patternfly.example', ['patternfly.form']);
+
+ angular.module( 'patternfly.example' ).controller( 'DemoCtrl', function( $scope ) {
+ $scope.messageArea1text = "Initial Text";
+ $scope.messageArea2text = "";
+ $scope.messageInput3text = "";
+
+ $scope.charsMaxLimitExceeded = false;
+
+ // 'tfId' will equal the id of the text area/input field which
+ // triggered the event
+ $scope.$on('overCharsMaxLimit', function (event, tfId) {
+ if(!$scope.charsMaxLimitExceeded){
+ $scope.charsMaxLimitExceeded = true;
+ }
+ });
+
+ // 'tfId' will equal the id of the text area/input field which
+ // triggered the event
+ $scope.$on('underCharsMaxLimit', function (event, tfId) {
+ if($scope.charsMaxLimitExceeded){
+ $scope.charsMaxLimitExceeded = false;
+ }
+ });
+
+ });
+
+
+
+*/
+
+angular.module('patternfly.form').directive('pfRemainingCharsCount', ["$timeout", function ($timeout) {
+ 'use strict';
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ scope: {
+ ngModel: "="
+ },
+ link: function ($scope, $element, $attributes) {
+ var charsMaxLimit = $attributes.charsMaxLimit;
+ var charsWarnRemaining = $attributes.charsWarnRemaining;
+ var countRemainingFld = angular.element(document.getElementById($attributes.countFld));
+ var blockInputAtMaxLimit = ($attributes.blockInputAtMaxLimit === 'true');
+ var checkCharactersRemaining = function () {
+ var charsLength = $scope.ngModel.length;
+ var remainingChars = charsMaxLimit - charsLength;
+
+ // trim if blockInputAtMaxLimit and over limit
+ if (blockInputAtMaxLimit && charsLength > charsMaxLimit) {
+ $scope.ngModel = $scope.ngModel.substring(0, charsMaxLimit);
+ charsLength = $scope.ngModel.length;
+ remainingChars = charsMaxLimit - charsLength;
+ }
+
+ // creating scope vars for unit testing
+ $scope.remainingChars = remainingChars;
+ $scope.remainingCharsWarning = (remainingChars <= charsWarnRemaining ? true : false);
+
+ countRemainingFld.text(remainingChars);
+ countRemainingFld.toggleClass('chars-warn-remaining-pf', remainingChars <= charsWarnRemaining);
+
+ if (remainingChars < 0) {
+ $scope.$emit('overCharsMaxLimit', $attributes.id);
+ } else {
+ $scope.$emit('underCharsMaxLimit', $attributes.id);
+ }
+ };
+
+ $scope.$watch('ngModel', function () {
+ checkCharactersRemaining();
+ });
+
+ $element.on('keypress', function (event) {
+ // Once the charsMaxLimit has been met or exceeded, prevent all keypresses from working
+ if (blockInputAtMaxLimit && $element.val().length >= charsMaxLimit) {
+ // Except backspace
+ if (event.keyCode !== 8) {
+ event.preventDefault();
+ }
+ }
+ });
+ }
+ };
+}]);
+;/**
+ * @ngdoc directive
+ * @name patternfly.modals.directive:pfAboutModal
+ *
+ * @description
+ * Directive for rendering modal windows.
+ *
+ * @param {string=} additionalInfo Text explaining the version or copyright
+ * @param {string=} copyright Product copyright information
+ * @param {string=} imgAlt The alt text for the corner grahpic
+ * @param {string=} imgSrc The source for the corner grahpic
+ * @param {boolean=} isOpen Flag indicating that the modal should be opened
+ * @param {function=} onClose Function to call when modal is closed
+ * @param {object=} productInfo data for the modal:
+ *
+ *
.product - the product label
+ *
.version - the product version
+ *
+ * @param {string=} title The product title for the modal
+ *
+ * @example
+
+
+
+
+
+
+
+
+ angular.module('patternfly.modals').controller('ModalCtrl', function ($scope) {
+ $scope.additionalInfo = "Donec consequat dignissim neque, sed suscipit quam egestas in. Fusce bibendum " +
+ "laoreet lectus commodo interdum. Vestibulum odio ipsum, tristique et ante vel, iaculis placerat nulla. " +
+ "Suspendisse iaculis urna feugiat lorem semper, ut iaculis risus tempus.";
+ $scope.copyright = "Trademark and Copyright Information";
+ $scope.imgAlt = "Patternfly Symbol";
+ $scope.imgSrc = "img/logo-alt.svg";
+ $scope.title = "Product Title";
+ $scope.productInfo = [
+ { name: 'Version', value: '1.0.0.0.20160819142038_51be77c' },
+ { name: 'Server Name', value: 'Localhost' },
+ { name: 'User Name', value: 'admin' },
+ { name: 'User Role', value: 'Administrator' }];
+ $scope.open = function () {
+ $scope.isOpen = true;
+ }
+ $scope.onClose = function() {
+ $scope.isOpen = false;
+ }
+ });
+
+
+ */
+angular.module('patternfly.modals')
+
+.directive("pfAboutModalTransclude", ["$parse", function ($parse) {
+ 'use strict';
+ return {
+ link: function (scope, element, attrs) {
+ element.append($parse(attrs.pfAboutModalTransclude)(scope));
+ }
+ };
+}])
+
+.directive('pfAboutModal', function () {
+ 'use strict';
+ return {
+ restrict: 'A',
+ scope: {
+ additionalInfo: '=?',
+ copyright: '=?',
+ close: "&onClose",
+ imgAlt: '=?',
+ imgSrc: '=?',
+ isOpen: '=?',
+ productInfo: '=',
+ title: '=?'
+ },
+ templateUrl: 'modals/about-modal.html',
+ transclude: true,
+ controller: ['$scope', '$uibModal', '$transclude', function ($scope, $uibModal, $transclude) {
+ if ($scope.isOpen === undefined) {
+ $scope.isOpen = false;
+ }
+
+ // The ui-bootstrap modal only supports either template or templateUrl as a way to specify the content.
+ // When the content is retrieved, it is compiled and linked against the provided scope by the $uibModal service.
+ // Unfortunately, there is no way to provide transclusion there.
+ //
+ // The solution below embeds a placeholder directive (i.e., pfAboutModalTransclude) to append the transcluded DOM.
+ // The transcluded DOM is from a different location than the modal, so it needs to be handed over to the
+ // placeholder directive. Thus, we're passing the actual DOM, not the parsed HTML.
+ $scope.openModal = function () {
+ $uibModal.open({
+ controller: ['$scope', '$uibModalInstance', 'content', function ($scope, $uibModalInstance, content) {
+ $scope.template = content;
+ $scope.close = function () {
+ $uibModalInstance.close();
+ };
+ $scope.$watch(
+ function () {
+ return $scope.isOpen;
+ },
+ function (newValue) {
+ if (newValue === false) {
+ $uibModalInstance.close();
+ }
+ }
+ );
+ }],
+ resolve: {
+ content: function () {
+ var transcludedContent;
+ $transclude(function (clone) {
+ transcludedContent = clone;
+ });
+ return transcludedContent;
+ }
+ },
+ scope: $scope,
+ templateUrl: "about-modal-template.html"
+ })
+ .result.then(
+ function () {
+ $scope.close(); // closed
+ },
+ function () {
+ $scope.close(); // dismissed
+ }
+ );
+ };
+ }],
+ link: function (scope, element, attrs) {
+ // watching isOpen attribute to dispay modal when needed
+ var isOpenListener = scope.$watch('isOpen', function (newVal, oldVal) {
+ if (newVal === true) {
+ scope.openModal();
+ }
+ });
+ scope.$on('$destroy', isOpenListener);
+ }
+ };
+});
+;/**
+ * @ngdoc directive
+ * @name patternfly.navigation.directive:pfApplicationLauncher
+ *
+ * @description
+ * Directive for rendering application launcher dropdown.
+ *
+ * @param {string=} label Use a custom label for the launcher, default: Application Launcher
+ * @param {boolean=} isDisabled Disable the application launcher button, default: false
+ * @param {boolean=} isList Display items as a list instead of a grid, default: false
+ * @param {boolean=} hiddenIcons Flag to not show icons on the launcher, default: false
+ * @param {array} items List of navigation items
+ *
+ *
.title - (string) Name of item to be displayed on the menu
+ *
.iconClass - (string) Classes for icon to be shown on the menu (ex. "fa fa-dashboard")
+ *
.href - (string) href link to navigate to on click
+ *
.tooltip - (string) Tooltip to display for the badge
+ *
+ * @example
+
+
+
+
+ Click the launcher indicator to show the Application Launcher Dropdown:
+
+
+
+
+
+
+
+
+
+
+
+ angular.module('patternfly.navigation').controller('applicationLauncherController', ['$scope',
+ function ($scope) {
+ $scope.navigationItems = [
+ {
+ title: "Recteque",
+ href: "#/ipsum/intellegam/recteque",
+ tooltip: "Launch the Function User Interface",
+ iconClass: "pficon-storage-domain"
+ },
+ {
+ title: "Suavitate",
+ href: "#/ipsum/intellegam/suavitate",
+ tooltip: "Launch the Function User Interface",
+ iconClass: "pficon-build"
+ },
+ {
+ title: "Lorem",
+ href: "#/ipsum/intellegam/lorem",
+ tooltip: "Launch the Function User Interface",
+ iconClass: "pficon-domain"
+ },
+ {
+ title: "Home",
+ href: "#/ipsum/intellegam/home",
+ tooltip: "Launch the Function User Interface",
+ iconClass: "pficon-home"
+ }
+ ];
+
+ $scope.label = 'Application Launcher';
+ $scope.isDisabled = false;
+ $scope.isList = false;
+ $scope.hiddenIcons = false;
+ }]);
+
+
+ */
+angular.module('patternfly.navigation').directive('pfApplicationLauncher', [
+ function () {
+ 'use strict';
+
+ return {
+ restrict: 'A',
+ scope: {
+ items: '&',
+ label: '@?',
+ isDisabled: '&?',
+ isList: '&?',
+ hiddenIcons: '&?'
+ },
+ templateUrl: 'navigation/application-launcher.html'
+ };
+ }]);
+
+;/**
+ * @ngdoc directive
+ * @name patternfly.navigation.directive:pfVerticalNavigation - Basic
+ *
+ * @description
+ * Directive for vertical navigation. This sets up the nav bar header with the collapse button (hamburger) and the
+ * application brand image (or text) as well as the vertical navigation bar containing the navigation items. This
+ * directive supports primary, secondary, and tertiary navigation with options to allow pinning of the secondary and
+ * tertiary navigation menus as well as the option for persistent secondary menus.
+ *
+ * The remaining parts of the navbar header can be transcluded.
+ *
+ * Tha navigation items are marked active based on the current location and the href value for the item. If not using
+ * href's on the items to navigate, set update-active-items-on-click to "true".
+ *
+ * This directive works in conjunction with the main content container if the 'container-pf-nav-pf-vertical' class
+ * selector is added to the main content container.
+ *
+ * @param {string} brandSrc src for brand image
+ * @param {string} brandAlt Text for product name when brand image is not available
+ * @param {boolean} showBadges Flag if badges are used on navigation items, default: false
+ * @param {boolean} persistentSecondary Flag to use persistent secondary menus, default: false
+ * @param {boolean} hiddenIcons Flag to not show icons on the primary menu, default: false
+ * @param {array} items List of navigation items
+ *
+ *
.title - (string) Name of item to be displayed on the menu
+ *
.iconClass - (string) Classes for icon to be shown on the menu (ex. "fa fa-dashboard")
+ *
.href - (string) href link to navigate to on click
+ *
.children - (array) Submenu items (same structure as top level items)
+ *
.badges - (array) Badges to display for the item, badges with a zero count are not displayed.
+ *
+ *
.count - (number) Count to display in the badge
+ *
.iconClass - (string) Class to use for showing an icon before the count
+ *
.tooltip - (string) Tooltip to display for the badge
+ *
.badgeClass: - (string) Additional class(es) to add to the badge container
+ *
+ *
+ * @param {function} navigateCallback function(item) Callback method invoked on a navigation item click (one with no submenus)
+ * @param {function} itemClickCallback function(item) Callback method invoked on an item click
+ * @param {boolean} updateActiveItemsOnClick Flag if active items should be marked on click rather than on navigation change, default: false
+ * @param {boolean} ignoreMobile Flag if mobile state should be ignored (use only if absolutely necessary) default: false
+ *
+ * @example
+
+
+
+
+ This will display the vertical nav bar and some mock content over the content of this page.
+ Exit the demo to return back to this page.
+
\
+ ';
+ });
+
+
+ $(document).ready(function() {
+ $(document).on('click', '#showVerticalNav', function() {
+ $(document.getElementById("verticalNavLayout")).removeClass("hidden");
+ });
+ $(document).on('click', '#hideVerticalNav', function() {
+ $(document.getElementById("verticalNavLayout")).addClass("hidden");
+ });
+ });
+
+
+*/
+;/**
+ * @ngdoc directive
+ * @name patternfly.navigation.directive:pfVerticalNavigation - Router
+ *
+ * @description
+ * This example shows how to use pfVerticalNavigation with angular-ui-router's $states and uiSrefs.
+ *
+ * @param {string} brandSrc src for brand image
+ * @param {string} brandAlt Text for product name when brand image is not available
+ * @param {boolean} showBadges Flag if badges are used on navigation items, default: false
+ * @param {boolean} persistentSecondary Flag to use persistent secondary menus, default: false
+ * @param {boolean} hiddenIcons Flag to not show icons on the primary menu, default: false
+ * @param {array} items List of navigation items
+ *
+ *
.title - (string) Name of item to be displayed on the menu
+ *
.iconClass - (string) Classes for icon to be shown on the menu (ex. "fa fa-dashboard")
+ *
.href - (string) href link to navigate to on click
+ *
.children - (array) Submenu items (same structure as top level items)
+ *
.badges - (array) Badges to display for the item, badges with a zero count are not displayed.
+ *
+ *
.count - (number) Count to display in the badge
+ *
.iconClass - (string) Class to use for showing an icon before the count
+ *
.tooltip - (string) Tooltip to display for the badge
+ *
.badgeClass: - (string) Additional class(es) to add to the badge container
+ *
+ *
.uiSref - (string) Optional Angular UI Router state name. If specified, href must be not defined, and vice versa.
+ *
.uiSrefOptions - (object) Optional object to be passed to Angular UI Router $state.go() function
+ *
+ * @param {function} navigateCallback function(item) Callback method invoked on a navigation item click (one with no submenus)
+ * @param {function} itemClickCallback function(item) Callback method invoked on an item click
+ * @param {boolean} updateActiveItemsOnClick Flag if active items should be marked on click rather than on navigation change, default: false
+ * @param {boolean} ignoreMobile Flag if mobile state should be ignored (use only if absolutely necessary) default: false
+ *
+ * @example
+
+
+
+
+ This will display the vertical nav bar and some mock content over the content of this page.
+ Exit the demo to return back to this page.
+
'
+ });
+ })
+ .controller('vertNavWithRouterController', ['$scope',
+ function ($scope) {
+ $scope.navigationItems = [
+ {
+ title: "Dashboard",
+ iconClass: "fa fa-dashboard",
+ uiSref: "dashboard",
+ uiSrefOptions: { someKey: 'SomeValue' }
+ },
+ {
+ title: "Dolor",
+ iconClass : "fa fa-shield",
+ uiSref: "dolor"
+ },
+ {
+ title: "Ipsum",
+ iconClass : "fa fa-space-shuttle",
+ uiSref: "ipsum"
+ },
+ {
+ title: "Exit Demo"
+ }
+ ];
+ $scope.handleNavigateClickRouter = function (item) {
+ if (item.title === "Exit Demo") {
+ angular.element(document.querySelector("#verticalNavWithRouterLayout")).addClass("hidden");
+ }
+ };
+ }
+ ]);
+
+
+ $(document).ready(function() {
+ $(document).on('click', '#showVerticalNavWithRouter', function() {
+ $(document.getElementById("verticalNavWithRouterLayout")).removeClass("hidden");
+ });
+ $(document).on('click', '#hideVerticalNavWithRouter', function() {
+ $(document.getElementById("verticalNavWithRouterLayout")).addClass("hidden");
+ });
+ });
+
+
+*/
+;angular.module('patternfly.navigation').directive('pfVerticalNavigation', ['$location', '$rootScope', '$window', '$document', '$timeout', '$injector',
+ function (location, rootScope, $window, $document, $timeout, $injector) {
+ 'use strict';
+ var $state;
+
+ // Optional dependency on $state
+ if ($injector.has("$state")) {
+ $state = $injector.get("$state");
+ }
+
+ return {
+ restrict: 'A',
+ scope: {
+ brandSrc: '@',
+ brandAlt: '@',
+ showBadges: '@',
+ persistentSecondary: '@',
+ pinnableMenus: '@',
+ hiddenIcons: '@',
+ items: '=',
+ navigateCallback: '=?',
+ itemClickCallback: '=?',
+ updateActiveItemsOnClick: '@',
+ ignoreMobile: '@'
+ },
+ replace: true,
+ templateUrl: 'navigation/vertical-navigation.html',
+ transclude: true,
+ controller: ["$scope", function ($scope) {
+ var routeChangeListener;
+
+ $scope.showBadges = $scope.showBadges === 'true';
+ $scope.persistentSecondary = $scope.persistentSecondary === 'true';
+ $scope.pinnableMenus = $scope.pinnableMenus === 'true';
+ $scope.hiddenIcons = $scope.hiddenIcons === 'true';
+ $scope.updateActiveItemsOnClick = $scope.updateActiveItemsOnClick === 'true';
+ $scope.ignoreMobile = $scope.ignoreMobile === 'true';
+ $scope.activeSecondary = false;
+
+ $scope.clearActiveItems = function () {
+ $scope.items.forEach(function (item) {
+ item.isActive = false;
+ if (item.children) {
+ item.children.forEach(function (secondary) {
+ secondary.isActive = false;
+ if (secondary.children) {
+ secondary.children.forEach(function (tertiary) {
+ tertiary.isActive = false;
+ });
+ }
+ });
+ }
+ });
+ };
+
+ $scope.setActiveItems = function () {
+ var updatedRoute = "#" + location.path();
+ //Setting active state on load
+ $scope.items.forEach(function (topLevel) {
+ if (updatedRoute.indexOf(topLevel.href) > -1) {
+ topLevel.isActive = true;
+ }
+ if (topLevel.children) {
+ topLevel.children.forEach(function (secondLevel) {
+ if (updatedRoute.indexOf(secondLevel.href) > -1) {
+ secondLevel.isActive = true;
+ topLevel.isActive = true;
+ }
+ if (secondLevel.children) {
+ secondLevel.children.forEach(function (thirdLevel) {
+ if (updatedRoute.indexOf(thirdLevel.href) > -1) {
+ thirdLevel.isActive = true;
+ secondLevel.isActive = true;
+ topLevel.isActive = true;
+ }
+ });
+ }
+ });
+ }
+ });
+ };
+
+ if (!$scope.updateActiveItemsOnClick) {
+ routeChangeListener = rootScope.$on( "$routeChangeSuccess", function (event, next, current) {
+ $scope.clearActiveItems();
+ $scope.setActiveItems();
+ });
+
+ $scope.$on('$destroy', routeChangeListener);
+ }
+ }],
+ link: function ($scope) {
+ var breakpoints = {
+ 'tablet': 768,
+ 'desktop': 1200
+ };
+
+ var getBodyContentElement = function () {
+ return angular.element(document.querySelector('.container-pf-nav-pf-vertical'));
+ };
+
+ var explicitCollapse = false;
+ var hoverDelay = 500;
+ var hideDelay = hoverDelay + 200;
+
+ var initBodyElement = function () {
+ var bodyContentElement = getBodyContentElement();
+ if ($scope.showBadges) {
+ bodyContentElement.addClass('nav-pf-vertical-with-badges');
+ }
+ if ($scope.persistentSecondary) {
+ bodyContentElement.addClass('nav-pf-persistent-secondary');
+ }
+ if ($scope.hiddenIcons) {
+ bodyContentElement.addClass('hidden-icons-pf');
+ }
+ };
+
+ var updateMobileMenu = function (selected, secondaryItem) {
+ $scope.items.forEach(function (item) {
+ item.isMobileItem = false;
+ if (item.children) {
+ item.children.forEach(function (nextSecondary) {
+ nextSecondary.isMobileItem = false;
+ });
+ }
+ });
+
+ if (selected) {
+ selected.isMobileItem = true;
+ if (secondaryItem) {
+ secondaryItem.isMobileItem = true;
+ $scope.showMobileSecondary = false;
+ $scope.showMobileTertiary = true;
+ } else {
+ $scope.showMobileSecondary = true;
+ $scope.showMobileTertiary = false;
+ }
+ } else {
+ $scope.showMobileSecondary = false;
+ $scope.showMobileTertiary = false;
+ }
+ };
+
+ var checkNavState = function () {
+ var width = $window.innerWidth;
+ var bodyContentElement = getBodyContentElement();
+
+ // Check to see if we need to enter/exit the mobile state
+ if (!$scope.ignoreMobile && width < breakpoints.tablet) {
+ if (!$scope.inMobileState) {
+ $scope.inMobileState = true;
+
+ //Set the body class to the correct state
+ bodyContentElement.removeClass('collapsed-nav');
+ bodyContentElement.addClass('hidden-nav');
+
+ // Reset the collapsed states
+ updateSecondaryCollapsedState(false);
+ updateTertiaryCollapsedState(false);
+
+ explicitCollapse = false;
+ }
+ } else {
+ $scope.inMobileState = false;
+ $scope.showMobileNav = false;
+
+ // Set the body class back to the default
+ bodyContentElement.removeClass('hidden-nav');
+ }
+
+ if (explicitCollapse) {
+ $scope.navCollapsed = true;
+ bodyContentElement.addClass('collapsed-nav');
+ } else {
+ $scope.navCollapsed = false;
+ bodyContentElement.removeClass('collapsed-nav');
+ }
+ };
+
+ var collapseMenu = function () {
+ var bodyContentElement = getBodyContentElement();
+ $scope.navCollapsed = true;
+
+ //Set the body class to the correct state
+ bodyContentElement.addClass('collapsed-nav');
+
+ explicitCollapse = true;
+ };
+
+ var expandMenu = function () {
+ var bodyContentElement = getBodyContentElement();
+ $scope.navCollapsed = false;
+
+ //Set the body class to the correct state
+ bodyContentElement.removeClass('collapsed-nav');
+
+ explicitCollapse = false;
+
+ // Dispatch a resize event when showing the expanding then menu to
+ // allow content to adjust to the menu sizing
+ angular.element($window).triggerHandler('resize');
+ };
+
+ var forceHideSecondaryMenu = function () {
+ $scope.forceHidden = true;
+ $timeout(function () {
+ $scope.forceHidden = false;
+ }, 500);
+ };
+
+ var setParentActive = function (item) {
+ $scope.items.forEach(function (topLevel) {
+ if (topLevel.children) {
+ topLevel.children.forEach(function (secondLevel) {
+ if (secondLevel === item) {
+ topLevel.isActive = true;
+ }
+ if (secondLevel.children) {
+ secondLevel.children.forEach(function (thirdLevel) {
+ if (thirdLevel === item) {
+ topLevel.isActive = true;
+ secondLevel.isActive = true;
+ }
+ });
+ }
+ });
+ }
+ });
+ };
+
+ var getFirstNavigateChild = function (item) {
+ var firstChild;
+ if (!item.children || item.children.length < 1) {
+ firstChild = item;
+ } else {
+ firstChild = getFirstNavigateChild(item.children[0]);
+ }
+ return firstChild;
+ };
+
+ var setSecondaryItemVisible = function () {
+ var bodyContentElement = getBodyContentElement();
+ $scope.activeSecondary = false;
+
+ if ($scope.persistentSecondary && !$scope.inMobileState) {
+ $scope.items.forEach(function (topLevel) {
+ if (topLevel.children) {
+ topLevel.children.forEach(function (secondLevel) {
+ if (secondLevel.isActive) {
+ $scope.activeSecondary = true;
+ }
+ });
+ }
+ });
+ if ($scope.activeSecondary) {
+ bodyContentElement.addClass('secondary-visible-pf');
+ } else {
+ bodyContentElement.removeClass('secondary-visible-pf');
+ }
+ }
+ };
+
+ var navigateToItem = function (item) {
+ var navItem = getFirstNavigateChild(item);
+ var navTo;
+ if (navItem) {
+ $scope.showMobileNav = false;
+ if (navItem.uiSref && navItem.href) {
+ throw new Error('Using both uiSref and href on an item is not supported.');
+ }
+ if (navItem.uiSref) {
+ if ($state === undefined) {
+ throw new Error('uiSref is defined on item, but no $state has been injected. ' +
+ 'Did you declare a dependency on "ui.router" module in your app?');
+ }
+ $state.go(navItem.uiSref, navItem.uiSrefOptions);
+ } else {
+ navTo = navItem.href;
+ if (navTo) {
+ if (navTo.startsWith('#/')) {
+ navTo = navTo.substring(2);
+ }
+ location.path(navTo);
+ }
+ }
+ if ($scope.navigateCallback) {
+ $scope.navigateCallback(navItem);
+ }
+ }
+
+ if ($scope.itemClickCallback) {
+ $scope.itemClickCallback(item);
+ }
+
+ if ($scope.updateActiveItemsOnClick ) {
+ $scope.clearActiveItems();
+ navItem.isActive = true;
+ setParentActive(navItem);
+ setSecondaryItemVisible();
+ }
+ setSecondaryItemVisible();
+ };
+
+ var primaryHover = function () {
+ var hover = false;
+ $scope.items.forEach(function (item) {
+ if (item.isHover) {
+ hover = true;
+ }
+ });
+ return hover;
+ };
+
+ var secondaryHover = function () {
+ var hover = false;
+ $scope.items.forEach(function (item) {
+ if (item.children && item.children.length > 0) {
+ item.children.forEach(function (secondaryItem) {
+ if (secondaryItem.isHover) {
+ hover = true;
+ }
+ });
+ }
+ });
+ return hover;
+ };
+
+ var updateSecondaryCollapsedState = function (setCollapsed, collapsedItem) {
+ var bodyContentElement = getBodyContentElement();
+ if (collapsedItem) {
+ collapsedItem.secondaryCollapsed = setCollapsed;
+ }
+ if (setCollapsed) {
+ $scope.collapsedSecondaryNav = true;
+
+ bodyContentElement.addClass('collapsed-secondary-nav-pf');
+ } else {
+ // Remove any collapsed secondary menus
+ if ($scope.items) {
+ $scope.items.forEach(function (item) {
+ item.secondaryCollasped = false;
+ });
+ }
+ $scope.collapsedSecondaryNav = false;
+
+ bodyContentElement.removeClass('collapsed-secondary-nav-pf');
+ }
+ };
+
+ var updateTertiaryCollapsedState = function (setCollapsed, collapsedItem) {
+ var bodyContentElement = getBodyContentElement();
+ if (collapsedItem) {
+ collapsedItem.tertiaryCollapsed = setCollapsed;
+ }
+ if (setCollapsed) {
+ $scope.collapsedTertiaryNav = true;
+
+ bodyContentElement.addClass('collapsed-tertiary-nav-pf');
+ updateSecondaryCollapsedState(false);
+ } else {
+ // Remove any collapsed secondary menus
+ if ($scope.items) {
+ $scope.items.forEach(function (item) {
+ if (item.children && item.children.length > 0) {
+ item.children.forEach(function (secondaryItem) {
+ secondaryItem.tertiaryCollasped = false;
+ });
+ }
+ });
+ }
+ $scope.collapsedTertiaryNav = false;
+
+ bodyContentElement.removeClass('collapsed-tertiary-nav-pf');
+ }
+ };
+
+ $scope.showMobileNav = false;
+ $scope.showMobileSecondary = false;
+ $scope.showMobileTertiary = false;
+ $scope.hoverSecondaryNav = false;
+ $scope.hoverTertiaryNav = false;
+ $scope.collapsedSecondaryNav = false;
+ $scope.collapsedTertiaryNav = false;
+ $scope.navCollapsed = false;
+ $scope.forceHidden = false;
+
+ $scope.handleNavBarToggleClick = function () {
+
+ if ($scope.inMobileState) {
+ // Toggle the mobile nav
+ if ($scope.showMobileNav) {
+ $scope.showMobileNav = false;
+ } else {
+ // Always start at the primary menu
+ updateMobileMenu();
+ $scope.showMobileNav = true;
+ }
+ } else if ($scope.navCollapsed) {
+ expandMenu();
+ } else {
+ collapseMenu();
+ }
+ };
+
+ $scope.handlePrimaryClick = function (item, event) {
+ if ($scope.inMobileState) {
+ if (item.children && item.children.length > 0) {
+ updateMobileMenu(item);
+ } else {
+ updateMobileMenu();
+ navigateToItem(item);
+ }
+ } else {
+ navigateToItem(item);
+ }
+ };
+
+ $scope.handleSecondaryClick = function (primary, secondary, event) {
+ if ($scope.inMobileState) {
+ if (secondary.children && secondary.children.length > 0) {
+ updateMobileMenu(primary, secondary);
+ } else {
+ updateMobileMenu();
+ navigateToItem(secondary);
+ }
+ } else {
+ navigateToItem(secondary);
+ }
+ };
+
+ $scope.handleTertiaryClick = function (primary, secondary, tertiary, event) {
+ if ($scope.inMobileState) {
+ updateMobileMenu();
+ }
+
+ navigateToItem(tertiary);
+ };
+
+ // Show secondary nav bar on hover of primary nav items
+ $scope.handlePrimaryHover = function (item) {
+ if (item.children && item.children.length > 0) {
+ if (!$scope.inMobileState) {
+ if (item.navUnHoverTimeout !== undefined) {
+ $timeout.cancel(item.navUnHoverTimeout);
+ item.navUnHoverTimeout = undefined;
+ } else if ($scope.navHoverTimeout === undefined && !item.isHover) {
+ item.navHoverTimeout = $timeout(function () {
+ $scope.hoverSecondaryNav = true;
+ item.isHover = true;
+ item.navHoverTimeout = undefined;
+ }, hoverDelay);
+ }
+ }
+ }
+ };
+
+ $scope.handlePrimaryUnHover = function (item) {
+ if (item.children && item.children.length > 0) {
+ if (item.navHoverTimeout !== undefined) {
+ $timeout.cancel(item.navHoverTimeout);
+ item.navHoverTimeout = undefined;
+ } else if (item.navUnHoverTimeout === undefined && item.isHover) {
+ item.navUnHoverTimeout = $timeout(function () {
+ item.isHover = false;
+ if (!primaryHover()) {
+ $scope.hoverSecondaryNav = false;
+ }
+ item.navUnHoverTimeout = undefined;
+ }, hideDelay);
+ }
+ }
+ };
+
+ // Show tertiary nav bar on hover of secondary nav items
+ $scope.handleSecondaryHover = function (item) {
+ if (item.children && item.children.length > 0) {
+ if (!$scope.inMobileState) {
+ if (item.navUnHoverTimeout !== undefined) {
+ $timeout.cancel(item.navUnHoverTimeout);
+ item.navUnHoverTimeout = undefined;
+ } else if ($scope.navHoverTimeout === undefined) {
+ item.navHoverTimeout = $timeout(function () {
+ $scope.hoverTertiaryNav = true;
+ item.isHover = true;
+ item.navHoverTimeout = undefined;
+ }, hoverDelay);
+ }
+ }
+ }
+ };
+
+ $scope.handleSecondaryUnHover = function (item) {
+ if (item.children && item.children.length > 0) {
+ if (item.navHoverTimeout !== undefined) {
+ $timeout.cancel(item.navHoverTimeout);
+ item.navHoverTimeout = undefined;
+ } else if (item.navUnHoverTimeout === undefined) {
+ item.navUnHoverTimeout = $timeout(function () {
+ item.isHover = false;
+ if (!secondaryHover()) {
+ $scope.hoverTertiaryNav = false;
+ }
+ item.navUnHoverTimeout = undefined;
+ }, hideDelay);
+ }
+ }
+ };
+
+ $scope.collapseSecondaryNav = function (item, event) {
+ if ($scope.inMobileState) {
+ updateMobileMenu();
+ } else {
+ if (item.secondaryCollapsed) {
+ updateSecondaryCollapsedState(false, item);
+ forceHideSecondaryMenu();
+ } else {
+ updateSecondaryCollapsedState(true, item);
+ }
+ }
+
+ $scope.hoverSecondaryNav = false;
+ event.stopImmediatePropagation();
+ };
+
+ $scope.collapseTertiaryNav = function (item, event) {
+ if ($scope.inMobileState) {
+ $scope.items.forEach(function (primaryItem) {
+ if (primaryItem.children) {
+ primaryItem.children.forEach(function (secondaryItem) {
+ if (secondaryItem === item) {
+ updateMobileMenu(primaryItem);
+ }
+ });
+ }
+ });
+ } else {
+ if (item.tertiaryCollapsed) {
+ updateTertiaryCollapsedState(false, item);
+ forceHideSecondaryMenu();
+ } else {
+ updateTertiaryCollapsedState(true, item);
+ }
+ }
+
+ $scope.hoverSecondaryNav = false;
+ $scope.hoverTertiaryNav = false;
+ event.stopImmediatePropagation();
+ };
+
+ initBodyElement();
+ checkNavState();
+
+ angular.element($window).on('resize', function () {
+ checkNavState();
+ $timeout(function () {
+ try {
+ $scope.$apply();
+ } catch (e) {
+ // Ignore, if we already applied, that is fine.
+ }
+ });
+ });
+ }
+ };
+ }]);
+
+;/**
+ * @ngdoc directive
+ * @name patternfly.notification.directive:pfInlineNotification
+ * @restrict E
+ * @scope
+ *
+ * @param {expression=} pfNotificationType The type of the notification message. Allowed value is one of these: 'success','info','danger', 'warning'.
+ * @param {expression=} pfNotificationMessage The main text message of the notification.
+ * @param {expression=} pfNotificationHeader The header text of the notification.
+ * @param {expression=} pfNotificationPersistent The notification won't disappear after delay timeout, but has to be closed manually with the close button.
+ *
+ * @description
+ * The main visual element of the notification message.
+ *
+ * @example
+
+
+
+
+
+
+
+
+
+
+ Header:
+
+
+
+
+
+ Message:
+
+
+
+
+
+ Type:
+
+
+
+
+
+ Persistent:
+
+
+
+
+
+
+
+
+
+ angular.module( 'patternfly.notification' ).controller( 'NotificationDemoCtrl', function( $scope, Notifications ) {
+ $scope.types = ['success','info','danger', 'warning'];
+ $scope.type = $scope.types[0];
+ $scope.isPersistent = false;
+
+ $scope.header = 'Default Header.';
+ $scope.message = 'Default Message.';
+ });
+
+
+
+ */
+angular.module( 'patternfly.notification' ).directive('pfInlineNotification', function () {
+ 'use strict';
+
+ return {
+ scope: {
+ 'pfNotificationType': '=',
+ 'pfNotificationMessage': '=',
+ 'pfNotificationHeader': '=',
+ 'pfNotificationPersistent': '=',
+ 'pfNotificationIndex': '='
+ },
+ restrict: 'E',
+ templateUrl: 'notification/inline-notification.html'
+ };
+});
+;/**
+ * @ngdoc directive
+ * @name patternfly.notification.directive:pfNotificationDrawer
+ *
+ * @description
+ * Directive for rendering a notification drawer. This provides a common mechanism to handle how the notification
+ * drawer should look and behave without mandating the look of the notification group heading or notification body.
+ *
+ * An array of notification groups must be passed to create each group in the drawer. Each notification
+ * group must include an array of notifications to be shown for that group, the array MUST be called 'notifications'.
+ * You must provide the source for the heading, sub-heading, and notification body to show the content you desire for each.
+ * Pass a customScope object containing any scope variables/functions you need to access from the included source, access these
+ * via hanlders. in your included source.
+ *
+ *
+ * @param {boolean} drawerHidden Flag if the drawer is currently hidden
+ * @param {boolean} allowExpand Flag if the drawer can be expanded. Optional, default: false
+ * @param {boolean} drawExpanded Flag if the drawer is expanded (only valid if allowExpand is true). Optional, default: false
+ * @param {string} drawerTitle Title to display for the drawer (leaving this blank will remove the provided expand capability)
+ * @param {object} notificationGroups Array of notification groups to add to the drawer
+ * @param {string} actionButtonTitle Text for the lower action button of the drawer (optional, if not specified there will be no action button)
+ * @param {function} actionButtonCallback function(notificationGroup) Callback method for action button for each group, the notificationGroup is passed (Optional)
+ * @param {string} titleInclude Include src for the title area for the notification drawer, use this to customize the drawer title area
+ * @param {string} headingInclude Include src for the heading area for each notification group, access the group via notificationGroup
+ * @param {string} subheadingInclude Include src for the sub-heading area for each notification group, access the group via notificationGroup
+ * @param {string} notificationBodyInclude Include src for the notification body for each notification, access the notification via notification
+ * @param {string} notificationFooterInclude Include src for the notification footer for each notification, access the notification via notification
+ * @param {object} customScope Object containing any variables/functions used by the included src, access via customScope.
+ *
+ * @example
+
+
+
+
+ Click the notifications indicator to show the Notification Drawer:
+
+
+
+
+ angular.module('patternfly.notification').controller( 'NotificationDemoCtrl', function( $scope, Notifications ) {
+ $scope.message = 'Default Message.';
+
+ var typeMap = { 'Info': Notifications.info,
+ 'Success': Notifications.success,
+ 'Warning': Notifications.warn,
+ 'Danger': Notifications.error };
+
+ $scope.types = Object.keys(typeMap);
+
+ $scope.type = $scope.types[0];
+ $scope.message = 'Default notification message.';
+
+ $scope.notify = function () {
+ typeMap[$scope.type]($scope.message);
+ }
+ });
+
+
+
+ */
+angular.module('patternfly.notification').directive('pfNotificationList', function () {
+ 'use strict';
+
+ NotificationListController.$inject = ["$scope", "$rootScope"];
+ return {
+ restrict: 'E',
+ controller: NotificationListController,
+ templateUrl: 'notification/notification-list.html'
+ };
+
+ function NotificationListController ($scope, $rootScope) {
+ $scope.notifications = $rootScope.notifications;
+ }
+});
+;/**
+ * @ngdoc directive
+ * @name patternfly.notification.directive:pfToastNotificationList
+ * @restrict A
+ * @scope
+ *
+ * @param {Array} notifications The list of current notifcations to display. Each notification should have the following (see pfToastNotification):
+ *
+ *
.type - (String) The type of the notification message. Allowed value is one of these: 'success','info','danger', 'warning'
+ *
.header - (String) The header to display for the notification (optional)
+ *
.message - (String) The main text message of the notification.
+ *
.actionTitle Text to show for the primary action, optional.
+ *
.actionCallback (function(this notification)) Function to invoke when primary action is selected, optional
+ *
.menuActions Optional list of actions to place in the kebab menu:
+ *
+ *
.name - (String) The name of the action, displayed on the button
+ *
.actionFn - (function(action, this notification)) Function to invoke when the action selected
+ *
.isDisabled - (Boolean) set to true to disable the action
+ *
.isSeparator - (Boolean) set to true if this is a placehodler for a separator rather than an action
+ *
+ *
.isPersistent Flag to show close button for the notification even if showClose is false.
+ *
+ * @param {Boolean} showClose Flag to show the close button on all notifications (not shown if the notification has menu actions)
+ * @param {function} closeCallback (function(data)) Function to invoke when closes a toast notification
+ * @param {function} updateViewing (function(boolean, data)) Function to invoke when user is viewing/not-viewing (hovering on) a toast notification
+ *
+ * @description
+ * Using this directive displayes a list of toast notifications
+ *
+ * @example
+
+
+
+
+
+
+
+
+
+ Show Close buttons:
+
+
+
+
+
+ Type:
+
+
+
+
+
+ Header:
+
+
+
+
+
+ Message:
+
+
+
+
+
+ Primary Action:
+
+
+
+
+
+ Persistent:
+
+
+
+ Show Menu:
+
+
+
+
+
+
+
+
+
+
+
+
+ Actions:
+
+
+
+
+
+
+
+
+
+ angular.module('patternfly.notification').controller( 'ToastNotificationListDemoCtrl', function( $scope, $rootScope, Notifications ) {
+ $scope.message = 'Default Message.';
+
+ var typeMap = { 'Info': 'info',
+ 'Success': 'success',
+ 'Warning': 'warning',
+ 'Danger': 'danger' };
+
+ $scope.types = Object.keys(typeMap);
+
+ $scope.type = $scope.types[0];
+ $scope.header = 'Default header.';
+ $scope.message = 'Default notification message.';
+ $scope.showClose = false;
+ $scope.persistent = false;
+
+ $scope.primaryAction = '';
+
+ $scope.showMenu = false;
+ var performAction = function (menuAction, data) {
+ $scope.actionText += menuAction.name + ": " + data.message + '\n';
+ };
+ $scope.menuActions = [
+ {
+ name: 'Action',
+ title: 'Perform an action',
+ actionFn: performAction
+ },
+ {
+ name: 'Another Action',
+ title: 'Do something else',
+ actionFn: performAction
+ },
+ {
+ name: 'Disabled Action',
+ title: 'Unavailable action',
+ actionFn: performAction,
+ isDisabled: true
+ },
+ {
+ name: 'Something Else',
+ title: '',
+ actionFn: performAction
+ },
+ {
+ isSeparator: true
+ },
+ {
+ name: 'Grouped Action 1',
+ title: 'Do something',
+ actionFn: performAction
+ },
+ {
+ name: 'Grouped Action 2',
+ title: 'Do something similar',
+ actionFn: performAction
+ }
+ ];
+
+ $scope.actionText = "";
+
+ $scope.handleAction = function (data) {
+ $scope.actionText = $scope.primaryAction + ": " + data.message + '\n' + $scope.actionText;
+ };
+ $scope.handleClose = function (data) {
+ $scope.actionText = "Closed: " + data.message + '\n'+ $scope.actionText;
+ Notifications.remove(data);
+ };
+ $scope.updateViewing = function (viewing, data) {
+ Notifications.setViewing(data, viewing);
+ };
+
+ $scope.notify = function () {
+ Notifications.message (
+ typeMap[$scope.type],
+ $scope.header,
+ $scope.message,
+ $scope.persistent,
+ $scope.handleClose,
+ $scope.primaryAction,
+ $scope.handleAction,
+ ($scope.showMenu ? $scope.menuActions : undefined)
+ );
+ }
+
+ $scope.notifications = Notifications.data;
+ });
+
+
+
+ */
+angular.module('patternfly.notification').directive('pfToastNotificationList', function () {
+ 'use strict';
+
+ return {
+ restrict: 'A',
+ scope: {
+ notifications: '=',
+ showClose: '=?',
+ closeCallback: '=?',
+ updateViewing: '=?'
+ },
+ templateUrl: 'notification/toast-notification-list.html',
+ controller: ["$scope", function ($scope) {
+ $scope.handleClose = function (notification) {
+ if (angular.isFunction($scope.closeCallback)) {
+ $scope.closeCallback(notification);
+ }
+ };
+ $scope.handleViewingChange = function (isViewing, notification) {
+ if (angular.isFunction($scope.updateViewing)) {
+ $scope.updateViewing(isViewing, notification);
+ }
+ };
+ }]
+ };
+});
+;/**
+ * @ngdoc directive
+ * @name patternfly.notification.directive:pfToastNotification
+ * @restrict E
+ * @scope
+ *
+ * @param {string} notificationType The type of the notification message. Allowed value is one of these: 'success','info','danger', 'warning'
+ * @param {string} header The header text of the notification.
+ * @param {string} message The main text message of the notification.
+ * @param {boolean} showClose Flag to show the close button, default: true
+ * @param {function} closeCallback (function(data)) Function to invoke when close action is selected, optional
+ * @param {string} actionTitle Text to show for the primary action, optional.
+ * @param {function} actionCallback (function(data)) Function to invoke when primary action is selected, optional
+ * @param {Array} menuActions Optional list of actions to place in the kebab menu:
+ *
+ *
.name - (String) The name of the action, displayed on the button
+ *
.actionFn - (function(action, data)) Function to invoke when the action selected
+ *
.isDisabled - (Boolean) set to true to disable the action
+ *
.isSeparator - (Boolean) set to true if this is a placehodler for a separator rather than an action
+ *
+ * @param {function} updateViewing (function(boolean, data)) Function to invoke when user is viewing/no-viewing (hovering on) the toast
+ * @param {object} data Any data needed by the callbacks (optional)
+ *
+ * @description
+ * Toast notifications are used to notify users of a system occurence. Toast notifications should be transient and stay on the screen for 8 seconds,
+ * so that they do not block the information behind them for too long, but allows the user to read the message.
+ * The pfToastNotification directive allows status, header, message, primary action and menu actions for the notification. The notification can also
+ * allow the user to close the notification.
+ *
+ * Note: Using the kebab menu (menu actions) with the close button is not currently supported. If both are specified the close button will not be shown.
+ * Add a close menu item if you want to have both capabilities.
+ *
+ * @example
+
+
+
+