/**
 * The iVantage Treeview module
 *
 * @package ivh.treeview
 */

angular.module('ivh.treeview', []);


/**
 * Supports non-default interpolation symbols
 *
 * @package ivh.treeview
 * @copyright 2016 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').constant('ivhTreeviewInterpolateEndSymbol', '}}');



/**
 * Supports non-default interpolation symbols
 *
 * @package ivh.treeview
 * @copyright 2016 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').constant('ivhTreeviewInterpolateStartSymbol', '{{');



/**
 * Selection management logic for treeviews with checkboxes
 *
 * @private
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').directive('ivhTreeviewCheckboxHelper', [function () {
    'use strict';
    return {
        restrict: 'A',
        scope: {
            node: '=ivhTreeviewCheckboxHelper'
        },
        require: '^ivhTreeview',
        link: function (scope, element, attrs, trvw) {
            var node = scope.node
              , opts = trvw.opts()
              , indeterminateAttr = opts.indeterminateAttribute
              , selectedAttr = opts.selectedAttribute;

            // Set initial selected state of this checkbox
            scope.isSelected = node[selectedAttr];

            // Local access to the parent controller
            scope.trvw = trvw;

            // Enforce consistent behavior across browsers by making indeterminate
            // checkboxes become checked when clicked/selected using spacebar
            scope.resolveIndeterminateClick = function () {
                if (node[indeterminateAttr]) {
                    trvw.select(node, true);
                }
            };

            // Update the checkbox when the node's selected status changes
            scope.$watch('node.' + selectedAttr, function (newVal, oldVal) {
                scope.isSelected = newVal;
            });

            // Update the checkbox when the node's indeterminate status changes
            scope.$watch('node.' + indeterminateAttr, function (newVal, oldVal) {
                element.find('input').prop('indeterminate', newVal);
            });
        },
        template: [
          '<input type="checkbox"',
            'class="ivh-treeview-checkbox"',
            'ng-model="isSelected"',
            'ng-click="resolveIndeterminateClick()"',
            'ng-change="trvw.select(node, isSelected)" />'
        ].join('\n')
    };
}]);



/**
 * Wrapper for a checkbox directive
 *
 * Basically exists so folks creeting custom node templates don't need to attach
 * their node to this directive explicitly - i.e. keeps consistent interface
 * with the twistie and toggle directives.
 *
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').directive('ivhTreeviewCheckbox', [function () {
    'use strict';
    return {
        restrict: 'AE',
        require: '^ivhTreeview',
        template: '<span ivh-treeview-checkbox-helper="node"></span>'
    };
}]);


/**
 * The recursive step, output child nodes for the scope node
 *
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').directive('ivhTreeviewChildren', function () {
    'use strict';
    return {
        restrict: 'AE',
        require: '^ivhTreeviewNode',
        template: [
          '<ul ng-if="getChildren().length" class="ivh-treeview">',
            '<li ng-repeat="child in getChildren()"',
                'ng-hide="trvw.hasFilter() && !trvw.isVisible(child)"',
                'class="ivh-treeview-node"',
                'ng-class="{\'ivh-treeview-node-collapsed\': !trvw.isExpanded(child) && !trvw.isLeaf(child)}"',
                'ivh-treeview-node="child"',
                'ivh-treeview-depth="childDepth">',
            '</li>',
          '</ul>'
        ].join('\n')
    };
});


/**
 * Treeview tree node directive
 *
 * @private
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').directive('ivhTreeviewNode', ['ivhTreeviewCompiler', function (ivhTreeviewCompiler) {
    'use strict';
    return {
        restrict: 'A',
        scope: {
            node: '=ivhTreeviewNode',
            depth: '=ivhTreeviewDepth'
        },
        require: '^ivhTreeview',
        compile: function (tElement) {
            return ivhTreeviewCompiler
              .compile(tElement, function (scope, element, attrs, trvw) {
                  var node = scope.node;

                  var getChildren = scope.getChildren = function () {
                      return trvw.children(node);
                  };

                  scope.trvw = trvw;
                  scope.childDepth = scope.depth + 1;

                  // Expand/collapse the node as dictated by the expandToDepth property.
                  // Note that we will respect the expanded state of this node if it has
                  // been expanded by e.g. `ivhTreeviewMgr.expandTo` but not yet
                  // rendered.
                  if (!trvw.isExpanded(node)) {
                      trvw.expand(node, trvw.isInitiallyExpanded(scope.depth));
                  }

                  /**
                   * @todo Provide a way to opt out of this
                   */
                  scope.$watch(function () {
                      return getChildren().length > 0;
                  }, function (newVal) {
                      if (newVal) {
                          element.removeClass('ivh-treeview-node-leaf');
                      } else {
                          element.addClass('ivh-treeview-node-leaf');
                      }
                  });
              });
        }
    };
}]);



/**
 * Toggle logic for treeview nodes
 *
 * Handles expand/collapse on click. Does nothing for leaf nodes.
 *
 * @private
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').directive('ivhTreeviewToggle', [function () {
    'use strict';
    return {
        restrict: 'A',
        require: '^ivhTreeview',
        link: function (scope, element, attrs, trvw) {
            var node = scope.node;

            element.addClass('ivh-treeview-toggle');

            element.bind('click', function () {
                if (!trvw.isLeaf(node)) {
                    scope.$apply(function () {
                        trvw.toggleExpanded(node);
                        trvw.onToggle(node);
                    });
                }
            });
        }
    };
}]);

/**
 * select label logic for treeview nodes
 * modify basis on business requirements 
 */
angular.module('ivh.treeview').directive('ivhTreeviewLabelSelect', [function () {
    'use strict';
    return {
        restrict: 'A',
        require: '^ivhTreeview',
        link: function (scope, element, attrs, trvw) {
            var node = scope.node;

            element.bind('click', function () {
                $('span').removeClass('label-active');
                element.toggleClass('label-active');
                scope.$apply(function () {
                    trvw.onLabelSelect(node);
                });
            });
        }
    };
}]);


/**
 * Treeview twistie directive
 *
 * @private
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').directive('ivhTreeviewTwistie', ['$compile', 'ivhTreeviewOptions', function ($compile, ivhTreeviewOptions) {
    'use strict';

    var globalOpts = ivhTreeviewOptions();

    return {
        restrict: 'A',
        require: '^ivhTreeview',
        template: [
          '<span class="ivh-treeview-twistie">',
            '<span class="ivh-treeview-twistie-collapsed">',
              globalOpts.twistieCollapsedTpl,
            '</span>',
            '<span class="ivh-treeview-twistie-expanded">',
              globalOpts.twistieExpandedTpl,
            '</span>',
            '<span class="ivh-treeview-twistie-leaf">',
              globalOpts.twistieLeafTpl,
            '</span>',
          '</span>'
        ].join('\n'),
        link: function (scope, element, attrs, trvw) {

            if (!trvw.hasLocalTwistieTpls) {
                return;
            }

            var opts = trvw.opts()
              , $twistieContainers = element
                .children().eq(0) // Template root
                .children(); // The twistie spans

            angular.forEach([
              // Should be in the same order as elements in template
              'twistieCollapsedTpl',
              'twistieExpandedTpl',
              'twistieLeafTpl'
            ], function (tplKey, ix) {
                var tpl = opts[tplKey]
                  , tplGlobal = globalOpts[tplKey];

                // Do nothing if we don't have a new template
                if (!tpl || tpl === tplGlobal) {
                    return;
                }

                // Super gross, the template must actually be an html string, we won't
                // try too hard to enforce this, just don't shoot yourself in the foot
                // too badly and everything will be alright.
                if (tpl.substr(0, 1) !== '<' || tpl.substr(-1, 1) !== '>') {
                    tpl = '<span>' + tpl + '</span>';
                }

                var $el = $compile(tpl)(scope)
                  , $container = $twistieContainers.eq(ix);

                // Clean out global template and append the new one
                $container.html('').append($el);
            });

        }
    };
}]);


/**
 * The `ivh-treeview` directive
 *
 * A filterable tree view with checkbox support.
 *
 * Example:
 *
 * ```
 * <div
 *   ivh-treeview="myHierarchicalData">
 *   ivh-treeview-filter="myFilterText">
 * </div>
 * ```
 *
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').directive('ivhTreeview', ['ivhTreeviewMgr', function (ivhTreeviewMgr) {
    'use strict';
    return {
        restrict: 'A',
        transclude: true,
        scope: {
            // The tree data store
            root: '=ivhTreeview',

            // Specific config options
            childrenAttribute: '=ivhTreeviewChildrenAttribute',
            defaultSelectedState: '=ivhTreeviewDefaultSelectedState',
            expandToDepth: '=ivhTreeviewExpandToDepth',
            idAttribute: '=ivhTreeviewIdAttribute',
            indeterminateAttribute: '=ivhTreeviewIndeterminateAttribute',
            expandedAttribute: '=ivhTreeviewExpandedAttribute',
            labelAttribute: '=ivhTreeviewLabelAttribute',
            nodeTpl: '=ivhTreeviewNodeTpl',
            selectedAttribute: '=ivhTreeviewSelectedAttribute',
            onCbChange: '&ivhTreeviewOnCbChange',
            onToggle: '&ivhTreeviewOnToggle',
            onLabelSelect: '&ivhTreeviewOnLabelSelect',
            twistieCollapsedTpl: '=ivhTreeviewTwistieCollapsedTpl',
            twistieExpandedTpl: '=ivhTreeviewTwistieExpandedTpl',
            twistieLeafTpl: '=ivhTreeviewTwistieLeafTpl',
            useCheckboxes: '=ivhTreeviewUseCheckboxes',
            validate: '=ivhTreeviewValidate',
            visibleAttribute: '=ivhTreeviewVisibleAttribute',
            showAccessControlPanel: '=ivhTreeviewShowAccessControlPanel',
            accessList: '=ivhTreeviewAccessList',
            // Generic options object
            userOptions: '=ivhTreeviewOptions',

            // The filter
            filter: '=ivhTreeviewFilter'
        },
        controllerAs: 'trvw',
        controller: ['$scope', '$element', '$attrs', '$transclude', 'ivhTreeviewOptions', 'filterFilter', function ($scope, $element, $attrs, $transclude, ivhTreeviewOptions, filterFilter) {
            var ng = angular
              , trvw = this;

            // Merge any locally set options with those registered with hte
            // ivhTreeviewOptions provider
            var localOpts = ng.extend({}, ivhTreeviewOptions(), $scope.userOptions);

            // Two-way bound attributes (=) can be copied over directly if they're
            // non-empty
            ng.forEach([
              'childrenAttribute',
              'defaultSelectedState',
              'expandToDepth',
              'idAttribute',
              'indeterminateAttribute',
              'expandedAttribute',
              'labelAttribute',
              'nodeTpl',
              'selectedAttribute',
              'twistieCollapsedTpl',
              'twistieExpandedTpl',
              'twistieLeafTpl',
              'useCheckboxes',
              'validate',
              'visibleAttribute',
              'showAccessControlPanel',
              'accessList'
            ], function (attr) {
                if (ng.isDefined($scope[attr])) {
                    localOpts[attr] = $scope[attr];
                }
            });

            // Attrs with the `&` prefix will yield a defined scope entity even if
            // no value is specified. We must check to make sure the attribute string
            // is non-empty before copying over the scope value.
            var normedAttr = function (attrKey) {
                return 'ivhTreeview' +
                  attrKey.charAt(0).toUpperCase() +
                  attrKey.slice(1);
            };

            ng.forEach([
              'onCbChange',
              'onToggle',
              'onLabelSelect'
            ], function (attr) {
                if ($attrs[normedAttr(attr)]) {
                    localOpts[attr] = $scope[attr];
                }
            });

            // Treat the transcluded content (if there is any) as our node template
            var transcludedScope;
            $transclude(function (clone, scope) {
                var transcludedNodeTpl = '';
                angular.forEach(clone, function (c) {
                    transcludedNodeTpl += (c.innerHTML || '').trim();
                });
                if (transcludedNodeTpl.length) {
                    transcludedScope = scope;
                    localOpts.nodeTpl = transcludedNodeTpl;
                }
            });

            /**
             * Get the merged global and local options
             *
             * @return {Object} the merged options
             */
            trvw.opts = function () {
                return localOpts;
            };

            // If we didn't provide twistie templates we'll be doing a fair bit of
            // extra checks for no reason. Let's just inform down stream directives
            // whether or not they need to worry about twistie non-global templates.
            var userOpts = $scope.userOptions || {};

            /**
             * Whether or not we have local twistie templates
             *
             * @private
             */
            trvw.hasLocalTwistieTpls = !!(
              userOpts.twistieCollapsedTpl ||
              userOpts.twistieExpandedTpl ||
              userOpts.twistieLeafTpl ||
              $scope.twistieCollapsedTpl ||
              $scope.twistieExpandedTpl ||
              $scope.twistieLeafTpl);

            /**
             * Get the child nodes for `node`
             *
             * Abstracts away the need to know the actual label attribute in
             * templates.
             *
             * @param {Object} node a tree node
             * @return {Array} the child nodes
             */
            trvw.children = function (node) {
                var children = node[localOpts.childrenAttribute];
                return ng.isArray(children) ? children : [];
            };

            /**
             * Get the label for `node`
             *
             * Abstracts away the need to know the actual label attribute in
             * templates.
             *
             * @param {Object} node A tree node
             * @return {String} The node label
             */
            trvw.label = function (node) {
                return node[localOpts.labelAttribute];
            };

            /**
             * Returns `true` if this treeview has a filter
             *
             * @return {Boolean} Whether on not we have a filter
             * @private
             */
            trvw.hasFilter = function () {
                return ng.isDefined($scope.filter);
            };

            /**
             * Get the treeview filter
             *
             * @return {String} The filter string
             * @private
             */
            trvw.getFilter = function () {
                return $scope.filter || '';
            };

            /**
             * Returns `true` if current filter should hide `node`, false otherwise
             *
             * @todo Note that for object and function filters each node gets hit with
             * `isVisible` N-times where N is its depth in the tree. We may be able to
             * optimize `isVisible` in this case by:
             *
             * - On first call to `isVisible` in a given digest cycle walk the tree to
             *   build a flat array of nodes.
             * - Run the array of nodes through the filter.
             * - Build a map (`id`/$scopeId --> true) for the nodes that survive the
             *   filter
             * - On subsequent calls to `isVisible` just lookup the node id in our
             *   map.
             * - Clean the map with a $timeout (?)
             *
             * In theory the result of a call to `isVisible` could change during a
             * digest cycle as scope variables are updated... I think calls would
             * happen bottom up (i.e. from "leaf" to "root") so that might not
             * actually be an issue. Need to investigate if this ends up feeling for
             * large/deep trees.
             *
             * @param {Object} node A tree node
             * @return {Boolean} Whether or not `node` is filtered out
             */
            trvw.isVisible = function (node) {
                var filter = trvw.getFilter();

                // Quick shortcut
                if (!filter || filterFilter([node], filter).length) {
                    return true;
                }

                // If we have an object or function filter we have to check children
                // separately
                if (typeof filter === 'object' || typeof filter === 'function') {
                    var children = trvw.children(node);
                    // If any child is visible then so is this node
                    for (var ix = children.length; ix--;) {
                        if (trvw.isVisible(children[ix])) {
                            return true;
                        }
                    }
                }

                return false;
            };

            /**
             * Returns `true` if we should use checkboxes, false otherwise
             *
             * @return {Boolean} Whether or not to use checkboxes
             */
            trvw.useCheckboxes = function () {
                return localOpts.useCheckboxes;
            };

             trvw.showAccessControlPanel = function () {
                return localOpts.showAccessControlPanel;
            };

             trvw.accessList = function () {
                return localOpts.accessList;
             };

            /**
             * Select or deselect `node`
             *
             * Updates parent and child nodes appropriately, `isSelected` defaults to
             * `true`.
             *
             * @param {Object} node The node to select or deselect
             * @param {Boolean} isSelected Defaults to `true`
             */
            trvw.select = function (node, isSelected) {
                ivhTreeviewMgr.select($scope.root, node, localOpts, isSelected);
                trvw.onCbChange(node, isSelected);
            };

            /**
             * Get the selected state of `node`
             *
             * @param {Object} node The node to get the selected state of
             * @return {Boolean} `true` if `node` is selected
             */
            trvw.isSelected = function (node) {
                return node[localOpts.selectedAttribute];
            };

            /**
             * Toggle the selected state of `node`
             *
             * Updates parent and child node selected states appropriately.
             *
             * @param {Object} node The node to update
             */
            trvw.toggleSelected = function (node) {
                var isSelected = !node[localOpts.selectedAttribute];
                trvw.select(node, isSelected);
            };

            /**
             * Expand or collapse a given node
             *
             * `isExpanded` is optional and defaults to `true`.
             *
             * @param {Object} node The node to expand/collapse
             * @param {Boolean} isExpanded Whether to expand (`true`) or collapse
             */
            trvw.expand = function (node, isExpanded) {
                ivhTreeviewMgr.expand($scope.root, node, localOpts, isExpanded);
            };

            /**
             * Get the expanded state of a given node
             *
             * @param {Object} node The node to check the expanded state of
             * @return {Boolean}
             */
            trvw.isExpanded = function (node) {
                return node[localOpts.expandedAttribute];
            };

            /**
             * Toggle the expanded state of a given node
             *
             * @param {Object} node The node to toggle
             */
            trvw.toggleExpanded = function (node) {
                trvw.expand(node, !trvw.isExpanded(node));
            };

            /**
             * Whether or not nodes at `depth` should be expanded by default
             *
             * Use -1 to fully expand the tree by default.
             *
             * @param {Integer} depth The depth to expand to
             * @return {Boolean} Whether or not nodes at `depth` should be expanded
             * @private
             */
            trvw.isInitiallyExpanded = function (depth) {
                var expandTo = localOpts.expandToDepth === -1 ?
                    Infinity : localOpts.expandToDepth;
                return depth < expandTo;
            };

            /**
             * Returns `true` if `node` is a leaf node
             *
             * @param {Object} node The node to check
             * @return {Boolean} `true` if `node` is a leaf
             */
            trvw.isLeaf = function (node) {
                return trvw.children(node).length === 0;
            };

            /**
             * Get the tree node template
             *
             * @return {String} The node template
             * @private
             */
            trvw.getNodeTpl = function () {
                return localOpts.nodeTpl;
            };

            /**
             * Get the root of the tree
             *
             * Mostly a helper for custom templates
             *
             * @return {Object|Array} The tree root
             * @private
             */
            trvw.root = function () {
                return $scope.root;
            };

            /**
             * Call the registered toggle handler
             *
             * Handler will get a reference to `node` and the root of the tree.
             *
             * @param {Object} node Tree node to pass to the handler
             * @private
             */
            trvw.onToggle = function (node) {
                if (localOpts.onToggle) {
                    var locals = {
                        ivhNode: node,
                        ivhIsExpanded: trvw.isExpanded(node),
                        ivhTree: $scope.root
                    };
                    localOpts.onToggle(locals);
                }
            };

            trvw.onLabelSelect = function (node) {
                if (localOpts.onLabelSelect) {
                    var locals = {
                        ivhNode: node,
                        ivhIsExpanded: trvw.isExpanded(node),
                        ivhTree: $scope.root
                    };
                    localOpts.onLabelSelect(locals);
                }
            };

            /**
             * Call the registered selection change handler
             *
             * Handler will get a reference to `node`, the new selected state of
             * `node, and the root of the tree.
             *
             * @param {Object} node Tree node to pass to the handler
             * @param {Boolean} isSelected Selected state for `node`
             * @private
             */
            trvw.onCbChange = function (node, isSelected) {
                if (localOpts.onCbChange) {
                    var locals = {
                        ivhNode: node,
                        ivhIsSelected: isSelected,
                        ivhTree: $scope.root
                    };
                    localOpts.onCbChange(locals);
                }
            };
        }],
        link: function (scope, element, attrs) {
            var opts = scope.trvw.opts();

            // Allow opt-in validate on startup
            if (opts.validate) {
                ivhTreeviewMgr.validate(scope.root, opts);
            }
        },
        template: [
          '<ul class="ivh-treeview">',
            '<li ng-repeat="child in root | ivhTreeviewAsArray"',
                'ng-hide="trvw.hasFilter() && !trvw.isVisible(child)"',
                'class="ivh-treeview-node"',
                'ng-class="{\'ivh-treeview-node-collapsed\': !trvw.isExpanded(child) && !trvw.isLeaf(child)}"',
                'ivh-treeview-node="child"',
                'ivh-treeview-depth="0">',
            '</li>',
          '</ul>'
        ].join('\n')
    };
}]);


angular.module('ivh.treeview').filter('ivhTreeviewAsArray', function () {
    'use strict';
    return function (arr) {
        if (!angular.isArray(arr) && angular.isObject(arr)) {
            return [arr];
        }
        return arr;
    };
});


/**
 * Breadth first searching for treeview data stores
 *
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').factory('ivhTreeviewBfs', ['ivhTreeviewOptions', function (ivhTreeviewOptions) {
    'use strict';

    var ng = angular;

    /**
     * Breadth first search of `tree`
     *
     * `opts` is optional and may override settings from `ivhTreeviewOptions.options`.
     * The callback `cb` will be invoked on each node in the tree as we traverse,
     * if it returns `false` traversal of that branch will not continue. The
     * callback is given the current node as the first parameter and the node
     * ancestors, from closest to farthest, as an array in the second parameter.
     *
     * @param {Array|Object} tree The tree data
     * @param {Object} opts [optional] Settings overrides
     * @param {Function} cb [optional] Callback to run against each node
     */
    return function (tree, opts, cb) {
        if (arguments.length === 2 && ng.isFunction(opts)) {
            cb = opts;
            opts = {};
        }
        opts = angular.extend({}, ivhTreeviewOptions(), opts);
        cb = cb || ng.noop;

        var queue = []
          , childAttr = opts.childrenAttribute
          , next, node, parents, ix, numChildren;

        if (ng.isArray(tree)) {
            ng.forEach(tree, function (n) {
                // node and parents
                queue.push([n, []]);
            });
            next = queue.shift();
        } else {
            // node and parents
            next = [tree, []];
        }

        while (next) {
            node = next[0];
            parents = next[1];
            // cb might return `undefined` so we have to actually check for equality
            // against `false`
            if (cb(node, parents) !== false) {
                if (node[childAttr] && ng.isArray(node[childAttr])) {
                    numChildren = node[childAttr].length;
                    for (ix = 0; ix < numChildren; ix++) {
                        queue.push([node[childAttr][ix], [node].concat(parents)]);
                    }
                }
            }
            next = queue.shift();
        }
    };
}]);


/**
 * Compile helper for treeview nodes
 *
 * Defers compilation until after linking parents. Otherwise our treeview
 * compilation process would recurse indefinitely.
 *
 * Thanks to http://stackoverflow.com/questions/14430655/recursion-in-angular-directives
 *
 * @private
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').factory('ivhTreeviewCompiler', ['$compile', function ($compile) {
    'use strict';
    return {
        /**
         * Manually compiles the element, fixing the recursion loop.
         * @param {Object} element The angular element or template
         * @param {Function} link [optional] A post-link function, or an object with function(s) registered via pre and post properties.
         * @returns An object containing the linking functions.
         */
        compile: function (element, link) {
            // Normalize the link parameter
            if (angular.isFunction(link)) {
                link = { post: link };
            }

            var compiledContents;
            return {
                pre: (link && link.pre) ? link.pre : null,
                /**
                 * Compiles and re-adds the contents
                 */
                post: function (scope, element, attrs, trvw) {
                    // Compile our template
                    if (!compiledContents) {
                        compiledContents = $compile(trvw.getNodeTpl());
                    }
                    // Add the compiled template
                    compiledContents(scope, function (clone) {
                        element.append(clone);
                    });

                    // Call the post-linking function, if any
                    if (link && link.post) {
                        link.post.apply(null, arguments);
                    }
                }
            };
        }
    };
}]);


/**
 * Manager for treeview data stores
 *
 * Used to assist treeview operations, e.g. selecting or validating a tree-like
 * collection.
 *
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview')
  .factory('ivhTreeviewMgr', ['ivhTreeviewOptions', 'ivhTreeviewBfs', function (ivhTreeviewOptions, ivhTreeviewBfs) {
      'use strict';

      var ng = angular
        , options = ivhTreeviewOptions()
        , exports = {};

      // The make* methods and validateParent need to be bound to an options
      // object
      var makeDeselected = function (node) {
          node[this.selectedAttribute] = false;
          node[this.indeterminateAttribute] = false;
      };

      var makeSelected = function (node) {
          node[this.selectedAttribute] = true;
          node[this.indeterminateAttribute] = false;
      };

      var validateParent = function (node) {
          var children = node[this.childrenAttribute]
            , selectedAttr = this.selectedAttribute
            , indeterminateAttr = this.indeterminateAttribute
            , numSelected = 0
            , numIndeterminate = 0;
          ng.forEach(children, function (n, ix) {
              if (n[selectedAttr]) {
                  numSelected++;
              } else {
                  if (n[indeterminateAttr]) {
                      numIndeterminate++;
                  }
              }
          });

          if (0 === numSelected && 0 === numIndeterminate) {
              node[selectedAttr] = false;
              node[indeterminateAttr] = false;
          } else if (numSelected === children.length) {
              node[selectedAttr] = true;
              node[indeterminateAttr] = false;
          } else {
              node[selectedAttr] = false;
              node[indeterminateAttr] = true;
          }
      };

      var findNode = function (tree, node, opts, cb) {
          var useId = isId(node)
            , proceed = true
            , idAttr = opts.idAttribute;

          // Our return values
          var foundNode = null
            , foundParents = [];

          ivhTreeviewBfs(tree, opts, function (n, p) {
              var isNode = proceed && (useId ?
                node === n[idAttr] :
                node === n);

              if (isNode) {
                  // I've been looking for you all my life
                  proceed = false;
                  foundNode = n;
                  foundParents = p;
              }

              return proceed;
          });

          return cb(foundNode, foundParents);
      };

      var isId = function (val) {
          return ng.isString(val) || ng.isNumber(val);
      };

      /**
       * Select (or deselect) a tree node
       *
       * This method will update the rest of the tree to account for your change.
       *
       * You may alternatively pass an id as `node`, in which case the tree will
       * be searched for your item.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object|String} node The node (or id) to (de)select
       * @param {Object} opts [optional] Options to override default options with
       * @param {Boolean} isSelected [optional] Whether or not to select `node`, defaults to `true`
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.select = function (tree, node, opts, isSelected) {
          if (arguments.length > 2) {
              if (typeof opts === 'boolean') {
                  isSelected = opts;
                  opts = {};
              }
          }
          opts = ng.extend({}, options, opts);
          isSelected = ng.isDefined(isSelected) ? isSelected : true;

          var useId = isId(node)
            , proceed = true
            , idAttr = opts.idAttribute;

          ivhTreeviewBfs(tree, opts, function (n, p) {
              var isNode = proceed && (useId ?
                node === n[idAttr] :
                node === n);

              if (isNode) {
                  // I've been looking for you all my life
                  proceed = false;

                  var cb = isSelected ?
                    makeSelected.bind(opts) :
                    makeDeselected.bind(opts);

                  ivhTreeviewBfs(n, opts, cb);
                  ng.forEach(p, validateParent.bind(opts));
              }

              return proceed;
          });

          return exports;
      };

      /**
       * Select all nodes in a tree
       *
       * `opts` will default to an empty object, `isSelected` defaults to `true`.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object} opts [optional] Default options overrides
       * @param {Boolean} isSelected [optional] Whether or not to select items
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.selectAll = function (tree, opts, isSelected) {
          if (arguments.length > 1) {
              if (typeof opts === 'boolean') {
                  isSelected = opts;
                  opts = {};
              }
          }

          opts = ng.extend({}, options, opts);
          isSelected = ng.isDefined(isSelected) ? isSelected : true;

          var selectedAttr = opts.selectedAttribute
            , indeterminateAttr = opts.indeterminateAttribute;

          ivhTreeviewBfs(tree, opts, function (node) {
              node[selectedAttr] = isSelected;
              node[indeterminateAttr] = false;
          });

          return exports;
      };

      /**
       * Select or deselect each of the passed items
       *
       * Eventually it would be nice if this did something more intelligent than
       * just calling `select` on each item in the array...
       *
       * @param {Object|Array} tree The tree data
       * @param {Array} nodes The array of nodes or node ids
       * @param {Object} opts [optional] Default options overrides
       * @param {Boolean} isSelected [optional] Whether or not to select items
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.selectEach = function (tree, nodes, opts, isSelected) {
          /**
           * @todo Surely we can do something better than this...
           */
          ng.forEach(nodes, function (node) {
              exports.select(tree, node, opts, isSelected);
          });
          return exports;
      };

      /**
       * Deselect a tree node
       *
       * Delegates to `ivhTreeviewMgr.select` with `isSelected` set to `false`.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object|String} node The node (or id) to (de)select
       * @param {Object} opts [optional] Options to override default options with
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.deselect = function (tree, node, opts) {
          return exports.select(tree, node, opts, false);
      };

      /**
       * Deselect all nodes in a tree
       *
       * Delegates to `ivhTreeviewMgr.selectAll` with `isSelected` set to `false`.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object} opts [optional] Default options overrides
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.deselectAll = function (tree, opts) {
          return exports.selectAll(tree, opts, false);
      };

      /**
       * Deselect each of the passed items
       *
       * Delegates to `ivhTreeviewMgr.selectEach` with `isSelected` set to
       * `false`.
       *
       * @param {Object|Array} tree The tree data
       * @param {Array} nodes The array of nodes or node ids
       * @param {Object} opts [optional] Default options overrides
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.deselectEach = function (tree, nodes, opts) {
          return exports.selectEach(tree, nodes, opts, false);
      };

      /**
       * Validate tree for parent/child selection consistency
       *
       * Assumes `bias` as default selected state. The first element with
       * `node.select !== bias` will be assumed correct. For example, if `bias` is
       * `true` (the default) we'll traverse the tree until we come to an
       * unselected node at which point we stop and deselect each of that node's
       * children (and their children, etc.).
       *
       * Indeterminate states will also be resolved.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object} opts [optional] Options to override default options with
       * @param {Boolean} bias [optional] Default selected state
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.validate = function (tree, opts, bias) {
          if (!tree) {
              // Guard against uninitialized trees
              return exports;
          }

          if (arguments.length > 1) {
              if (typeof opts === 'boolean') {
                  bias = opts;
                  opts = {};
              }
          }
          opts = ng.extend({}, options, opts);
          bias = ng.isDefined(bias) ? bias : opts.defaultSelectedState;

          var selectedAttr = opts.selectedAttribute
            , indeterminateAttr = opts.indeterminateAttribute;

          ivhTreeviewBfs(tree, opts, function (node, parents) {
              if (ng.isDefined(node[selectedAttr]) && node[selectedAttr] !== bias) {
                  exports.select(tree, node, opts, !bias);
                  return false;
              } else {
                  node[selectedAttr] = bias;
                  node[indeterminateAttr] = false;
              }
          });

          return exports;
      };

      /**
       * Expand/collapse a given tree node
       *
       * `node` may be either an actual tree node object or a node id.
       *
       * `opts` may override any of the defaults set by `ivhTreeviewOptions`.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object|String} node The node (or id) to expand/collapse
       * @param {Object} opts [optional] Options to override default options with
       * @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true`
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.expand = function (tree, node, opts, isExpanded) {
          if (arguments.length > 2) {
              if (typeof opts === 'boolean') {
                  isExpanded = opts;
                  opts = {};
              }
          }
          opts = ng.extend({}, options, opts);
          isExpanded = ng.isDefined(isExpanded) ? isExpanded : true;

          var useId = isId(node)
            , expandedAttr = opts.expandedAttribute;

          if (!useId) {
              // No need to do any searching if we already have the node in hand
              node[expandedAttr] = isExpanded;
              return exports;
          }

          return findNode(tree, node, opts, function (n, p) {
              n[expandedAttr] = isExpanded;
              return exports;
          });
      };

      /**
       * Expand/collapse a given tree node and its children
       *
       * `node` may be either an actual tree node object or a node id. You may
       * leave off `node` entirely to expand/collapse the entire tree, however, if
       * you specify a value for `opts` or `isExpanded` you must provide a value
       * for `node`.
       *
       * `opts` may override any of the defaults set by `ivhTreeviewOptions`.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object|String} node [optional*] The node (or id) to expand/collapse recursively
       * @param {Object} opts [optional] Options to override default options with
       * @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true`
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.expandRecursive = function (tree, node, opts, isExpanded) {
          if (arguments.length > 2) {
              if (typeof opts === 'boolean') {
                  isExpanded = opts;
                  opts = {};
              }
          }
          node = ng.isDefined(node) ? node : tree;
          opts = ng.extend({}, options, opts);
          isExpanded = ng.isDefined(isExpanded) ? isExpanded : true;

          var useId = isId(node)
            , expandedAttr = opts.expandedAttribute
            , branch;

          // If we have an ID first resolve it to an actual node in the tree
          if (useId) {
              findNode(tree, node, opts, function (n, p) {
                  branch = n;
              });
          } else {
              branch = node;
          }

          if (branch) {
              ivhTreeviewBfs(branch, opts, function (n, p) {
                  n[expandedAttr] = isExpanded;
              });
          }

          return exports;
      };

      /**
       * Collapse a given tree node
       *
       * Delegates to `exports.expand` with `isExpanded` set to `false`.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object|String} node The node (or id) to collapse
       * @param {Object} opts [optional] Options to override default options with
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.collapse = function (tree, node, opts) {
          return exports.expand(tree, node, opts, false);
      };

      /**
       * Collapse a given tree node and its children
       *
       * Delegates to `exports.expandRecursive` with `isExpanded` set to `false`.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object|String} node The node (or id) to expand/collapse recursively
       * @param {Object} opts [optional] Options to override default options with
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.collapseRecursive = function (tree, node, opts, isExpanded) {
          return exports.expandRecursive(tree, node, opts, false);
      };

      /**
       * Expand[/collapse] all parents of a given node, i.e. "reveal" the node
       *
       * @param {Object|Array} tree The tree data
       * @param {Object|String} node The node (or id) to expand to
       * @param {Object} opts [optional] Options to override default options with
       * @param {Boolean} isExpanded [optional] Whether or not to expand parent nodes
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.expandTo = function (tree, node, opts, isExpanded) {
          if (arguments.length > 2) {
              if (typeof opts === 'boolean') {
                  isExpanded = opts;
                  opts = {};
              }
          }
          opts = ng.extend({}, options, opts);
          isExpanded = ng.isDefined(isExpanded) ? isExpanded : true;

          var expandedAttr = opts.expandedAttribute;

          var expandCollapseNode = function (n) {
              n[expandedAttr] = isExpanded;
          };

          // Even if wer were given the actual node and not its ID we must still
          // traverse the tree to find that node's parents.
          return findNode(tree, node, opts, function (n, p) {
              ng.forEach(p, expandCollapseNode);
              return exports;
          });
      };

      /**
       * Collapse all parents of a give node
       *
       * Delegates to `exports.expandTo` with `isExpanded` set to `false`.
       *
       * @param {Object|Array} tree The tree data
       * @param {Object|String} node The node (or id) to expand to
       * @param {Object} opts [optional] Options to override default options with
       * @return {Object} Returns the ivhTreeviewMgr instance for chaining
       */
      exports.collapseParents = function (tree, node, opts) {
          return exports.expandTo(tree, node, opts, false);
      };

      return exports;
  }
  ]);


/**
 * Global options for ivhTreeview
 *
 * @package ivh.treeview
 * @copyright 2014 iVantage Health Analytics, Inc.
 */

angular.module('ivh.treeview').provider('ivhTreeviewOptions', [
    'ivhTreeviewInterpolateStartSymbol', 'ivhTreeviewInterpolateEndSymbol',
    function (ivhTreeviewInterpolateStartSymbol, ivhTreeviewInterpolateEndSymbol) {
        'use strict';

        var symbolStart = ivhTreeviewInterpolateStartSymbol
          , symbolEnd = ivhTreeviewInterpolateEndSymbol;

        var options = {
            /**
             * ID attribute
             *
             * For selecting nodes by identifier rather than reference
             */
            idAttribute: 'id',

            /**
             * Collection item attribute to use for labels
             */
            labelAttribute: 'label',

            /**
             * Collection item attribute to use for child nodes
             */
            childrenAttribute: 'children',

            /**
             * Collection item attribute to use for selected state
             */
            selectedAttribute: 'selected',

            /**
             * Controls whether branches are initially expanded or collapsed
             *
             * A value of `0` means the tree will be entirely collapsd (the default
             * state) otherwise branches will be expanded up to the specified depth. Use
             * `-1` to have the tree entirely expanded.
             */
            expandToDepth: 0,

            /**
             * Whether or not to use checkboxes
             *
             * If `false` the markup to support checkboxes is not included in the
             * directive.
             */
            useCheckboxes: true,

            /**
             * Whether or not directive should validate treestore on startup
             */
            validate: true,

            /**
             * Collection item attribute to track intermediate states
             */
            indeterminateAttribute: '__ivhTreeviewIndeterminate',

            /**
             * Collection item attribute to track expanded status
             */
            expandedAttribute: '__ivhTreeviewExpanded',

            /**
             * Default selected state when validating
             */
            defaultSelectedState: true,

            /**
             * Template for expanded twisties
             */
            twistieExpandedTpl: '(-)',

            /**
             * Template for collapsed twisties
             */
            twistieCollapsedTpl: '(+)',

            /**
             * Template for leaf twisties (i.e. no children)
             */
            twistieLeafTpl: 'o',

            showAccessControlPanel: false,

            accessList:[],

            /**
             * Template for tree nodes
             * '<span class="ivh-treeview-node-label" ivh-treeview-toggle>',
             */
            nodeTpl: [
              '<div class="ivh-treeview-node-content" title="{{trvw.label(node)}}">',
                '<span ivh-treeview-toggle>',
                  '<span class="ivh-treeview-twistie-wrapper" ivh-treeview-twistie></span>',
                '</span>',
                '<span class="ivh-treeview-checkbox-wrapper" ng-if="trvw.useCheckboxes()"',
                    'ivh-treeview-checkbox>',
                '</span>',
                '<span class="ivh-treeview-node-label" ivh-treeview-label-select>',
                  '{{trvw.label(node)}}',
                '</span>',

                '<span style="padding:2px 20px;" ng-if=" trvw.showAccessControlPanel()">|',
                   '<span ng-repeat="item in trvw.accessList()">',
                        '<input ng-model="item" type="checkbox"/>{{item}}',
                   '</span>',
                '</span>',

                '<div ivh-treeview-children></div>',
              '</div>'
            ].join('\n')
            .replace(new RegExp('{{', 'g'), symbolStart)
            .replace(new RegExp('}}', 'g'), symbolEnd)
        };

        /**
         * Update global options
         *
         * @param {Object} opts options object to override defaults with
         */
        this.set = function (opts) {
            angular.extend(options, opts);
        };

        this.$get = function () {
            /**
             * Get a copy of the global options
             *
             * @return {Object} The options object
             */
            return function () {
                return angular.copy(options);
            };
        };
    }]);

angular.module('cgNotify', []).factory('notify',['$timeout','$http','$compile','$templateCache','$rootScope',
    function($timeout,$http,$compile,$templateCache,$rootScope){

        var startTop = 10;
        var verticalSpacing = 15;
        var defaultDuration = 10000;
        var defaultTemplateUrl = 'angular-notify.html';
        var position = 'center';
        var container = document.body;
        var maximumOpen = 0;

        var messageElements = [];
        var openNotificationsScope = [];

        var notify = function(args){

            if (typeof args !== 'object'){
                args = {message:args};
            }

            args.duration = args.duration ? args.duration : defaultDuration;
            args.templateUrl = args.templateUrl ? args.templateUrl : defaultTemplateUrl;
            args.container = args.container ? args.container : container;
            args.classes = args.classes ? args.classes : '';

            var scope = args.scope ? args.scope.$new() : $rootScope.$new();
            scope.$position = args.position ? args.position : position;
            scope.$message = args.message;
            scope.$classes = args.classes;
            scope.$messageTemplate = args.messageTemplate;

            if (maximumOpen > 0) {
                var numToClose = (openNotificationsScope.length + 1) - maximumOpen;
                for (var i = 0; i < numToClose; i++) {
                    openNotificationsScope[i].$close();
                }
            }

            $http.get(args.templateUrl,{cache: $templateCache}).then(function(template){

                var templateElement = $compile(template.data)(scope);
                templateElement.bind('webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd', function(e){
                    if (e.propertyName === 'opacity' || e.currentTarget.style.opacity === 0 || 
                        (e.originalEvent && e.originalEvent.propertyName === 'opacity')){

                        templateElement.remove();
                        messageElements.splice(messageElements.indexOf(templateElement),1);
                        openNotificationsScope.splice(openNotificationsScope.indexOf(scope),1);
                        layoutMessages();
                    }
                });

                if (args.messageTemplate){
                    var messageTemplateElement;
                    for (var i = 0; i < templateElement.children().length; i ++){
                        if (angular.element(templateElement.children()[i]).hasClass('cg-notify-message-template')){
                            messageTemplateElement = angular.element(templateElement.children()[i]);
                            break;
                        }
                    }
                    if (messageTemplateElement){
                        messageTemplateElement.append($compile(args.messageTemplate)(scope));
                    } else {
                        throw new Error('cgNotify could not find the .cg-notify-message-template element in '+args.templateUrl+'.');
                    }
                }

                angular.element(args.container).append(templateElement);
                messageElements.push(templateElement);

                if (scope.$position === 'center'){
                    $timeout(function(){
                        scope.$centerMargin = '-' + (templateElement[0].offsetWidth /2) + 'px';
                    });
                }

                scope.$close = function(){
                    templateElement.css('opacity',0).attr('data-closing','true');
                    layoutMessages();
                };

                var layoutMessages = function(){
                    var j = 0;
                    var currentY = startTop;
                    for(var i = messageElements.length - 1; i >= 0; i --){
                        var shadowHeight = 10;
                        var element = messageElements[i];
                        var height = element[0].offsetHeight;
                        var top = currentY + height + shadowHeight;
                        if (element.attr('data-closing')){
                            top += 20;
                        } else {
                            currentY += height + verticalSpacing;
                        }
                        element.css('top',top + 'px').css('margin-top','-' + (height+shadowHeight) + 'px').css('visibility','visible');
                        j ++;
                    }
                };

                $timeout(function(){
                    layoutMessages();
                });

                if (args.duration > 0){
                    $timeout(function(){
                        scope.$close();
                    },args.duration);
                }

            }, function(data) {
                    throw new Error('Template specified for cgNotify ('+args.templateUrl+') could not be loaded. ' + data);
            });

            var retVal = {};
            
            retVal.close = function(){
                if (scope.$close){
                    scope.$close();
                }
            };

            Object.defineProperty(retVal,'message',{
                get: function(){
                    return scope.$message;
                },
                set: function(val){
                    scope.$message = val;
                }
            });

            openNotificationsScope.push(scope);

            return retVal;

        };

        notify.config = function(args){
            startTop = !angular.isUndefined(args.startTop) ? args.startTop : startTop;
            verticalSpacing = !angular.isUndefined(args.verticalSpacing) ? args.verticalSpacing : verticalSpacing;
            defaultDuration = !angular.isUndefined(args.duration) ? args.duration : defaultDuration;
            defaultTemplateUrl = args.templateUrl ? args.templateUrl : defaultTemplateUrl;
            position = !angular.isUndefined(args.position) ? args.position : position;
            container = args.container ? args.container : container;
            maximumOpen = args.maximumOpen ? args.maximumOpen : maximumOpen;
        };

        notify.closeAll = function(){
            for(var i = messageElements.length - 1; i >= 0; i --){
                var element = messageElements[i];
                element.css('opacity',0);
            }
        };

        return notify;
    }
]);

angular.module('cgNotify').run(['$templateCache', function($templateCache) {
  'use strict';

  $templateCache.put('angular-notify.html',
    "<div class=\"cg-notify-message\" ng-class=\"[$classes, \n" +
    "    $position === 'center' ? 'cg-notify-message-center' : '',\n" +
    "    $position === 'left' ? 'cg-notify-message-left' : '',\n" +
    "    $position === 'right' ? 'cg-notify-message-right' : '']\"\n" +
    "    ng-style=\"{'margin-left': $centerMargin}\">\n" +
    "\n" +
    "    <div ng-show=\"!$messageTemplate\">\n" +
    "        {{$message}}\n" +
    "    </div>\n" +
    "\n" +
    "    <div ng-show=\"$messageTemplate\" class=\"cg-notify-message-template\">\n" +
    "        \n" +
    "    </div>\n" +
    "\n" +
    "    <button type=\"button\" class=\"cg-notify-close\" ng-click=\"$close()\">\n" +
    "        <span aria-hidden=\"true\">&times;</span>\n" +
    "        <span class=\"cg-notify-sr-only\">Close</span>\n" +
    "    </button>\n" +
    "\n" +
    "</div>"
  );

}]);

/**
 * ng-inline-edit v0.7.0 (http://tamerayd.in/ng-inline-edit)
 * Copyright 2015 Tamer Aydin (http://tamerayd.in)
 * Licensed under MIT
 */
(function(window, angular, undefined) {
  'use strict';

  angular
    .module('angularInlineEdit.providers', [])
    .value('InlineEditConfig', {
      btnEdit: 'Edit',
      btnSave: '',
      btnCancel: '',
      editOnClick: false,
      onBlur: null
    })
    .constant('InlineEditConstants', {
      CANCEL: 'cancel',
      SAVE: 'save'
    });

})(window, window.angular);

(function(window, angular, undefined) {
  'use strict';

  angular
    .module('angularInlineEdit.controllers', [])
    .controller('InlineEditController', ['$scope', '$document', '$timeout',
      function($scope, $document, $timeout) {
        $scope.placeholder = '';
        $scope.validationError = false;
        $scope.validating = false;
        $scope.isOnBlurBehaviorValid = false;
        $scope.cancelOnBlur = false;
        $scope.editMode = false;
        $scope.inputValue = '';

        $scope.editText = function(inputValue) {
          $scope.editMode = true;
          $scope.inputValue = (typeof inputValue === 'string') ?
            inputValue : $scope.model;

          $timeout(function() {
            $scope.editInput[0].focus();
            if ($scope.isOnBlurBehaviorValid) {
              $document.bind('click', $scope.onDocumentClick);
            }
          }, 0);
        };

        $scope.applyText = function(cancel, byDOM) {
          var inputValue = $scope.inputValue; // initial input value
          $scope.validationError = false;

          function _onSuccess() {
            $scope.model = inputValue;
            $scope.callback({
              newValue: inputValue
            });

            $scope.editMode = false;
          }

          function _onFailure() {
            $scope.validationError = true;
            $timeout(function() {
              $scope.editText(inputValue);
            }, 0);
          }

          function _onEnd(apply) {
            $scope.validating = false;
            if (apply && byDOM) {
              $scope.$apply();
            }
          }

          if (cancel || $scope.model === inputValue) {
            $scope.editMode = false;
            if (byDOM) {
              $scope.$apply();
            }

          } else {
            $scope.validating = true;
            if (byDOM) {
              $scope.$apply();
            }

            var validationResult = $scope.validate({
                newValue: $scope.inputValue
              });

            if (validationResult && validationResult.then) { // promise
              validationResult
                .then(_onSuccess)
                .catch(_onFailure)
                .finally(_onEnd);

            } else if (validationResult ||
                typeof validationResult === 'undefined') {
              _onSuccess();
              _onEnd(true);

            } else {
              _onFailure();
              _onEnd(true);
            }
          }

          if ($scope.isOnBlurBehaviorValid) {
            $document.unbind('click', $scope.onDocumentClick);
          }
        };

        $scope.onInputKeyup = function(event) {
          if (!$scope.validating) {
            switch (event.keyCode) {
              case 13: // ENTER
                if ($scope.isInputTextarea) {
                  return;
                }
                $scope.applyText(false, false);
                break;
              case 27: // ESC
                $scope.applyText(true, false);
                break;
              default:
                break;
            }
          }
        };

        $scope.onDocumentClick = function(event) {
          if (!$scope.validating) {
            if (event.target !== $scope.editInput[0]) {
              $scope.applyText($scope.cancelOnBlur, true);
            }
          }
        };
      }
    ]);

})(window, window.angular);

(function(window, angular, undefined) {
  'use strict';

  angular
    .module('angularInlineEdit.directives', [
      'angularInlineEdit.providers',
      'angularInlineEdit.controllers'
    ])
    .directive('inlineEdit', ['$compile', 'InlineEditConfig', 'InlineEditConstants',
      function($compile, InlineEditConfig, InlineEditConstants) {
        return {
          restrict: 'A',
          controller: 'InlineEditController',
          scope: {
            model: '=inlineEdit',
            callback: '&inlineEditCallback',
            validate: '&inlineEditValidation',
            remove: '&inlineEditDelete',
            hasEditPermission: '=hasEditPermission'
          },
          link: function(scope, element, attrs) {
            scope.model = scope.$parent.$eval(attrs.inlineEdit);
            scope.isInputTextarea = attrs.hasOwnProperty('inlineEditTextarea');

            var onBlurBehavior = attrs.hasOwnProperty('inlineEditOnBlur') ?
              attrs.inlineEditOnBlur : InlineEditConfig.onBlur;
            if (onBlurBehavior === InlineEditConstants.CANCEL ||
                onBlurBehavior === InlineEditConstants.SAVE) {
              scope.isOnBlurBehaviorValid = true;
              scope.cancelOnBlur = onBlurBehavior === InlineEditConstants.CANCEL;
            }

            var container = angular.element(
              '<div class="ng-inline-edit" ' +
                'ng-class="{\'ng-inline-edit--validating\': validating, ' +
                  '\'ng-inline-edit--error\': validationError}">');

            var input = angular.element(
              (scope.isInputTextarea ?
                '<textarea ' : '<input type="text" ') +
                'class="ng-inline-edit__input" ' +
                'ng-disabled="validating" ' +
                'ng-show="editMode" ' +
                'ng-keyup="onInputKeyup($event)" ' +
                'ng-model="inputValue" ' +
                'placeholder="{{placeholder}}" />');

            var innerContainer = angular.element(
              '<div class="ng-inline-edit__inner-container"></div>');

            // text
            innerContainer.append(angular.element(
              '<span class="ng-inline-edit__text" ' +
                'ng-class="{\'ng-inline-edit__text--placeholder\': !model}" ' +
                (attrs.hasOwnProperty('inlineEditOnClick') || InlineEditConfig.editOnClick ?
                  'ng-click="editText()" ' : '') +
                'ng-if="!editMode">{{(model || placeholder)' +
                  (attrs.hasOwnProperty('inlineEditFilter') ? ' | ' + attrs.inlineEditFilter : '') +
                  '}}</span>'));

            // edit button
            var inlineEditBtnEdit = attrs.hasOwnProperty('inlineEditBtnEdit') ?
              attrs.inlineEditBtnEdit : InlineEditConfig.btnEdit;
            if (inlineEditBtnEdit) {
              innerContainer.append(angular.element(
                '<span class="ng-custom-inline-edit"> '+
                  '<a class="ng-inline-edit__button ng-inline-edit__button--edit" ' +
                      'ng-if="!editMode && hasEditPermission" ' +
                      'ng-click="editText(model)">' +
                         inlineEditBtnEdit +
                  '</a>&nbsp;<a ng-if="!editMode && hasEditPermission" ng-click="remove()" class="ng-inline-delete_button ng-inline-edit__button ng-inline-edit__button--edit"> ' +
                  '<i class="fa fa-trash-o icon-role-category-delete" style="color: #ed6e3c"></i></a><span>'));
            }

            // save button
            var inlineEditBtnSave = attrs.hasOwnProperty('inlineEditBtnSave') ?
              attrs.inlineEditBtnSave : InlineEditConfig.btnSave;
            if (inlineEditBtnSave) {
              innerContainer.append(angular.element(
                '<a class="ng-inline-edit__button ng-inline-edit__button--save" ' +
                  'ng-if="editMode && !validating" ' +
                  'ng-click="applyText(false, false)">' +
                    inlineEditBtnSave +
                '</a>'));
            }

            // cancel button
            var inlineEditBtnCancel = attrs.hasOwnProperty('inlineEditBtnCancel') ?
              attrs.inlineEditBtnCancel : InlineEditConfig.btnCancel;
            if (inlineEditBtnCancel) {
              innerContainer.append(angular.element(
                '<a class="ng-inline-edit__button ng-inline-edit__button--cancel" ' +
                  'ng-if="editMode && !validating" ' +
                  'ng-click="applyText(true, false)">' +
                    inlineEditBtnCancel +
                '</a>'));
            }

            container
              .append(input)
              .append(innerContainer);

            element
              .append(container);

            scope.editInput = input;

            attrs.$observe('inlineEdit', function(newValue) {
              scope.model = scope.$parent.$eval(newValue);
              $compile(element.contents())(scope);
            });

            attrs.$observe('inlineEditPlaceholder', function(placeholder) {
              scope.placeholder = placeholder;
            });

            scope.$watch('model', function(newValue) {
              if (!isNaN(parseFloat(newValue)) && isFinite(newValue) && newValue === 0) {
                scope.model = '0';
              }
            });
          }
        };
      }
    ]);

})(window, window.angular);

(function(window, angular, undefined) {
  'use strict';

  angular
    .module('angularInlineEdit', [
      'angularInlineEdit.providers',
      'angularInlineEdit.controllers',
      'angularInlineEdit.directives'
    ]);

})(window, window.angular);