You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@falcon.apache.org by so...@apache.org on 2015/10/13 02:04:00 UTC

[07/22] falcon git commit: FALCON-1315 Update falcon ui for HiveDR, secure clusters and bug fixes. Contributed by Armando Reyna/Venkat Ranganathan.

http://git-wip-us.apache.org/repos/asf/falcon/blob/86180d93/falcon-ui/app/js/directives/ng-tags-input.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/directives/ng-tags-input.js b/falcon-ui/app/js/directives/ng-tags-input.js
new file mode 100644
index 0000000..1039011
--- /dev/null
+++ b/falcon-ui/app/js/directives/ng-tags-input.js
@@ -0,0 +1,1148 @@
+/*!
+ * ngTagsInput v2.3.0
+ * http://mbenford.github.io/ngTagsInput
+ *
+ * Copyright (c) 2013-2015 Michael Benford
+ * License: MIT
+ *
+ * Generated at 2015-03-24 00:49:44 -0300
+ */
+(function() {
+  'use strict';
+
+  var KEYS = {
+    backspace: 8,
+    tab: 9,
+    enter: 13,
+    escape: 27,
+    space: 32,
+    up: 38,
+    down: 40,
+    left: 37,
+    right: 39,
+    delete: 46,
+    comma: 188
+  };
+
+  var MAX_SAFE_INTEGER = 9007199254740991;
+  var SUPPORTED_INPUT_TYPES = ['text', 'email', 'url'];
+
+  var tagsInput = angular.module('ngTagsInput', []);
+
+  /**
+   * @ngdoc directive
+   * @name tagsInput
+   * @module ngTagsInput
+   *
+   * @description
+   * Renders an input box with tag editing support.
+   *
+   * @param {string} ngModel Assignable angular expression to data-bind to.
+   * @param {string=} [displayProperty=text] Property to be rendered as the tag label.
+   * @param {string=} [keyProperty=text] Property to be used as a unique identifier for the tag.
+   * @param {string=} [type=text] Type of the input element. Only 'text', 'email' and 'url' are supported values.
+   * @param {number=} tabindex Tab order of the control.
+   * @param {string=} [placeholder=Add a tag] Placeholder text for the control.
+   * @param {number=} [minLength=3] Minimum length for a new tag.
+   * @param {number=} [maxLength=MAX_SAFE_INTEGER] Maximum length allowed for a new tag.
+   * @param {number=} [minTags=0] Sets minTags validation error key if the number of tags added is less than minTags.
+   * @param {number=} [maxTags=MAX_SAFE_INTEGER] Sets maxTags validation error key if the number of tags added is greater than maxTags.
+   * @param {boolean=} [allowLeftoverText=false] Sets leftoverText validation error key if there is any leftover text in
+   *                                             the input element when the directive loses focus.
+   * @param {string=} [removeTagSymbol=×] Symbol character for the remove tag button.
+   * @param {boolean=} [addOnEnter=true] Flag indicating that a new tag will be added on pressing the ENTER key.
+   * @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key.
+   * @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key.
+   * @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus.
+   * @param {boolean=} [addOnPaste=false] Flag indicating that the text pasted into the input field will be split into tags.
+   * @param {string=} [pasteSplitPattern=,] Regular expression used to split the pasted text into tags.
+   * @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes.
+   * @param {string=} [allowedTagsPattern=.+] Regular expression that determines whether a new tag is valid.
+   * @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into
+   *                                                the new tag input box instead of being removed when the backspace key
+   *                                                is pressed and the input box is empty.
+   * @param {boolean=} [addFromAutocompleteOnly=false] Flag indicating that only tags coming from the autocomplete list will be allowed.
+   *                                                   When this flag is true, addOnEnter, addOnComma, addOnSpace, addOnBlur and
+   *                                                   allowLeftoverText values are ignored.
+   * @param {boolean=} [spellcheck=true] Flag indicating whether the browser's spellcheck is enabled for the input field or not.
+   * @param {expression} onTagAdding Expression to evaluate that will be invoked before adding a new tag. The new tag is available as $tag. This method must return either true or false. If false, the tag will not be added.
+   * @param {expression} onTagAdded Expression to evaluate upon adding a new tag. The new tag is available as $tag.
+   * @param {expression} onInvalidTag Expression to evaluate when a tag is invalid. The invalid tag is available as $tag.
+   * @param {expression} onTagRemoving Expression to evaluate that will be invoked before removing a tag. The tag is available as $tag. This method must return either true or false. If false, the tag will not be removed.
+   * @param {expression} onTagRemoved Expression to evaluate upon removing an existing tag. The removed tag is available as $tag.
+   */
+  tagsInput.directive('tagsInput', ["$timeout","$document","$window","tagsInputConfig","tiUtil", function($timeout, $document, $window, tagsInputConfig, tiUtil) {
+    function TagList(options, events, onTagAdding, onTagRemoving) {
+      var self = {}, getTagText, setTagText, tagIsValid;
+
+      getTagText = function(tag) {
+        return tiUtil.safeToString(tag[options.displayProperty]);
+      };
+
+      setTagText = function(tag, text) {
+        tag[options.displayProperty] = text;
+      };
+
+      tagIsValid = function(tag) {
+        var tagText = getTagText(tag);
+
+        return tagText &&
+            tagText.length >= options.minLength &&
+            tagText.length <= options.maxLength &&
+            options.allowedTagsPattern.test(tagText) &&
+            !tiUtil.findInObjectArray(self.items, tag, options.keyProperty || options.displayProperty) &&
+            onTagAdding({ $tag: tag });
+      };
+
+      self.items = [];
+
+      self.addText = function(text) {
+        var tag = {};
+        setTagText(tag, text);
+        return self.add(tag);
+      };
+
+      self.add = function(tag) {
+        var tagText = getTagText(tag);
+
+        if (options.replaceSpacesWithDashes) {
+          tagText = tiUtil.replaceSpacesWithDashes(tagText);
+        }
+
+        setTagText(tag, tagText);
+
+        if (tagIsValid(tag)) {
+          self.items.push(tag);
+          events.trigger('tag-added', { $tag: tag });
+        }
+        else if (tagText) {
+          events.trigger('invalid-tag', { $tag: tag });
+        }
+
+        return tag;
+      };
+
+      self.remove = function(index) {
+        var tag = self.items[index];
+
+        if (onTagRemoving({ $tag: tag }))  {
+          self.items.splice(index, 1);
+          self.clearSelection();
+          events.trigger('tag-removed', { $tag: tag });
+          return tag;
+        }
+      };
+
+      self.select = function(index) {
+        if (index < 0) {
+          index = self.items.length - 1;
+        }
+        else if (index >= self.items.length) {
+          index = 0;
+        }
+
+        self.index = index;
+        self.selected = self.items[index];
+      };
+
+      self.selectPrior = function() {
+        self.select(--self.index);
+      };
+
+      self.selectNext = function() {
+        self.select(++self.index);
+      };
+
+      self.removeSelected = function() {
+        return self.remove(self.index);
+      };
+
+      self.clearSelection = function() {
+        self.selected = null;
+        self.index = -1;
+      };
+
+      self.clearSelection();
+
+      return self;
+    }
+
+    function validateType(type) {
+      return SUPPORTED_INPUT_TYPES.indexOf(type) !== -1;
+    }
+
+    return {
+      restrict: 'E',
+      require: 'ngModel',
+      scope: {
+        tags: '=ngModel',
+        onTagAdding: '&',
+        onTagAdded: '&',
+        onInvalidTag: '&',
+        onTagRemoving: '&',
+        onTagRemoved: '&'
+      },
+      replace: false,
+      transclude: true,
+      templateUrl: 'ngTagsInput/tags-input.html',
+      controller: ["$scope","$attrs","$element", function($scope, $attrs, $element) {
+        $scope.events = tiUtil.simplePubSub();
+
+        tagsInputConfig.load('tagsInput', $scope, $attrs, {
+          template: [String, 'ngTagsInput/tag-item.html'],
+          type: [String, 'text', validateType],
+          placeholder: [String, 'Add a tag'],
+          tabindex: [Number, null],
+          removeTagSymbol: [String, String.fromCharCode(215)],
+          replaceSpacesWithDashes: [Boolean, true],
+          minLength: [Number, 3],
+          maxLength: [Number, MAX_SAFE_INTEGER],
+          addOnEnter: [Boolean, true],
+          addOnSpace: [Boolean, false],
+          addOnComma: [Boolean, true],
+          addOnBlur: [Boolean, true],
+          addOnPaste: [Boolean, false],
+          pasteSplitPattern: [RegExp, /,/],
+          allowedTagsPattern: [RegExp, /.+/],
+          enableEditingLastTag: [Boolean, false],
+          minTags: [Number, 0],
+          maxTags: [Number, MAX_SAFE_INTEGER],
+          displayProperty: [String, 'text'],
+          keyProperty: [String, ''],
+          allowLeftoverText: [Boolean, false],
+          addFromAutocompleteOnly: [Boolean, false],
+          spellcheck: [Boolean, true]
+        });
+
+        $scope.tagList = new TagList($scope.options, $scope.events,
+            tiUtil.handleUndefinedResult($scope.onTagAdding, true),
+            tiUtil.handleUndefinedResult($scope.onTagRemoving, true));
+
+        this.registerAutocomplete = function() {
+          var input = $element.find('input');
+
+          return {
+            addTag: function(tag) {
+              return $scope.tagList.add(tag);
+            },
+            focusInput: function() {
+              input[0].focus();
+            },
+            getTags: function() {
+              return $scope.tags;
+            },
+            getCurrentTagText: function() {
+              return $scope.newTag.text;
+            },
+            getOptions: function() {
+              return $scope.options;
+            },
+            on: function(name, handler) {
+              $scope.events.on(name, handler);
+              return this;
+            }
+          };
+        };
+
+        this.registerTagItem = function() {
+          return {
+            getOptions: function() {
+              return $scope.options;
+            },
+            removeTag: function(index) {
+              if ($scope.disabled) {
+                return;
+              }
+              $scope.tagList.remove(index);
+            }
+          };
+        };
+      }],
+      link: function(scope, element, attrs, ngModelCtrl) {
+        var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace, KEYS.delete, KEYS.left, KEYS.right],
+            tagList = scope.tagList,
+            events = scope.events,
+            options = scope.options,
+            input = element.find('input'),
+            validationOptions = ['minTags', 'maxTags', 'allowLeftoverText'],
+            setElementValidity;
+
+        setElementValidity = function() {
+          ngModelCtrl.$setValidity('maxTags', scope.tags.length <= options.maxTags);
+          ngModelCtrl.$setValidity('minTags', scope.tags.length >= options.minTags);
+          ngModelCtrl.$setValidity('leftoverText', scope.hasFocus || options.allowLeftoverText ? true : !scope.newTag.text);
+        };
+
+        ngModelCtrl.$isEmpty = function(value) {
+          return !value || !value.length;
+        };
+
+        scope.newTag = {
+          text: '',
+          invalid: null,
+          setText: function(value) {
+            this.text = value;
+            events.trigger('input-change', value);
+          }
+        };
+
+        scope.track = function(tag) {
+          return tag[options.keyProperty || options.displayProperty];
+        };
+
+        scope.$watch('tags', function(value) {
+          scope.tags = tiUtil.makeObjectArray(value, options.displayProperty);
+          tagList.items = scope.tags;
+        });
+
+        scope.$watch('tags.length', function() {
+          setElementValidity();
+        });
+
+        attrs.$observe('disabled', function(value) {
+          scope.disabled = value;
+        });
+
+        scope.eventHandlers = {
+          input: {
+            change: function(text) {
+              events.trigger('input-change', text);
+            },
+            keydown: function($event) {
+              events.trigger('input-keydown', $event);
+            },
+            focus: function() {
+              if (scope.hasFocus) {
+                return;
+              }
+
+              scope.hasFocus = true;
+              events.trigger('input-focus');
+            },
+            blur: function() {
+              $timeout(function() {
+                var activeElement = $document.prop('activeElement'),
+                    lostFocusToBrowserWindow = activeElement === input[0],
+                    lostFocusToChildElement = element[0].contains(activeElement);
+
+                if (lostFocusToBrowserWindow || !lostFocusToChildElement) {
+                  scope.hasFocus = false;
+                  events.trigger('input-blur');
+                }
+              });
+            },
+            paste: function($event) {
+              $event.getTextData = function() {
+                var clipboardData = $event.clipboardData || ($event.originalEvent && $event.originalEvent.clipboardData);
+                return clipboardData ? clipboardData.getData('text/plain') : $window.clipboardData.getData('Text');
+              };
+              events.trigger('input-paste', $event);
+            }
+          },
+          host: {
+            click: function() {
+              if (scope.disabled) {
+                return;
+              }
+              input[0].focus();
+            }
+          }
+        };
+
+        events
+            .on('tag-added', scope.onTagAdded)
+            .on('invalid-tag', scope.onInvalidTag)
+            .on('tag-removed', scope.onTagRemoved)
+            .on('tag-added', function() {
+              scope.newTag.setText('');
+            })
+            .on('tag-added tag-removed', function() {
+              // Sets the element to its dirty state
+              // In Angular 1.3 this will be replaced with $setDirty.
+              ngModelCtrl.$setViewValue(scope.tags);
+            })
+            .on('invalid-tag', function() {
+              scope.newTag.invalid = true;
+            })
+            .on('option-change', function(e) {
+              if (validationOptions.indexOf(e.name) !== -1) {
+                setElementValidity();
+              }
+            })
+            .on('input-change', function() {
+              tagList.clearSelection();
+              scope.newTag.invalid = null;
+            })
+            .on('input-focus', function() {
+              element.triggerHandler('focus');
+              ngModelCtrl.$setValidity('leftoverText', true);
+            })
+            .on('input-blur', function() {
+              if (options.addOnBlur && !options.addFromAutocompleteOnly) {
+                tagList.addText(scope.newTag.text);
+              }
+              element.triggerHandler('blur');
+              setElementValidity();
+            })
+            .on('input-keydown', function(event) {
+              var key = event.keyCode,
+                  isModifier = event.shiftKey || event.altKey || event.ctrlKey || event.metaKey,
+                  addKeys = {},
+                  shouldAdd, shouldRemove, shouldSelect, shouldEditLastTag;
+
+              if (isModifier || hotkeys.indexOf(key) === -1) {
+                return;
+              }
+
+              addKeys[KEYS.enter] = options.addOnEnter;
+              addKeys[KEYS.comma] = options.addOnComma;
+              addKeys[KEYS.space] = options.addOnSpace;
+
+              shouldAdd = !options.addFromAutocompleteOnly && addKeys[key];
+              shouldRemove = (key === KEYS.backspace || key === KEYS.delete) && tagList.selected;
+              shouldEditLastTag = key === KEYS.backspace && scope.newTag.text.length === 0 && options.enableEditingLastTag;
+              shouldSelect = (key === KEYS.backspace || key === KEYS.left || key === KEYS.right) && scope.newTag.text.length === 0 && !options.enableEditingLastTag;
+
+              if (shouldAdd) {
+                tagList.addText(scope.newTag.text);
+              }
+              else if (shouldEditLastTag) {
+                var tag;
+
+                tagList.selectPrior();
+                tag = tagList.removeSelected();
+
+                if (tag) {
+                  scope.newTag.setText(tag[options.displayProperty]);
+                }
+              }
+              else if (shouldRemove) {
+                tagList.removeSelected();
+              }
+              else if (shouldSelect) {
+                if (key === KEYS.left || key === KEYS.backspace) {
+                  tagList.selectPrior();
+                }
+                else if (key === KEYS.right) {
+                  tagList.selectNext();
+                }
+              }
+
+              if (shouldAdd || shouldSelect || shouldRemove || shouldEditLastTag) {
+                event.preventDefault();
+              }
+            })
+            .on('input-paste', function(event) {
+              if (options.addOnPaste) {
+                var data = event.getTextData();
+                var tags = data.split(options.pasteSplitPattern);
+
+                if (tags.length > 1) {
+                  tags.forEach(function(tag) {
+                    tagList.addText(tag);
+                  });
+                  event.preventDefault();
+                }
+              }
+            });
+      }
+    };
+  }]);
+
+
+  /**
+   * @ngdoc directive
+   * @name tiTagItem
+   * @module ngTagsInput
+   *
+   * @description
+   * Represents a tag item. Used internally by the tagsInput directive.
+   */
+  tagsInput.directive('tiTagItem', ["tiUtil", function(tiUtil) {
+    return {
+      restrict: 'E',
+      require: '^tagsInput',
+      template: '<ng-include src="$$template"></ng-include>',
+      scope: { data: '=' },
+      link: function(scope, element, attrs, tagsInputCtrl) {
+        var tagsInput = tagsInputCtrl.registerTagItem(),
+            options = tagsInput.getOptions();
+
+        scope.$$template = options.template;
+        scope.$$removeTagSymbol = options.removeTagSymbol;
+
+        scope.$getDisplayText = function() {
+          var label = tiUtil.safeToString(scope.data[options.displayProperty]);
+          label = label.replace("Name:","").replace("Type:","").replace("Tag:","");
+          return label;
+        };
+
+        scope.getDisplayLabel = function () {
+          var label = tiUtil.safeToString(scope.data[options.displayProperty]);
+          if(label.indexOf("Name:") !== -1){
+            label = "Name:";
+          }else if(label.indexOf("Type:") !== -1){
+            label = "Type:";
+          }else if(label.indexOf("Tag:") !== -1){
+            label = "Tag:";
+          }else{
+            label = "";
+          }
+          return label;
+        };
+
+        scope.$removeTag = function() {
+          tagsInput.removeTag(scope.$index);
+        };
+
+        scope.$watch('$parent.$index', function(value) {
+          scope.$index = value;
+        });
+      }
+    };
+  }]);
+
+
+  /**
+   * @ngdoc directive
+   * @name autoComplete
+   * @module ngTagsInput
+   *
+   * @description
+   * Provides autocomplete support for the tagsInput directive.
+   *
+   * @param {expression} source Expression to evaluate upon changing the input content. The input value is available as
+   *                            $query. The result of the expression must be a promise that eventually resolves to an
+   *                            array of strings.
+   * @param {string=} [displayProperty=text] Property to be rendered as the autocomplete label.
+   * @param {number=} [debounceDelay=100] Amount of time, in milliseconds, to wait before evaluating the expression in
+   *                                      the source option after the last keystroke.
+   * @param {number=} [minLength=3] Minimum number of characters that must be entered before evaluating the expression
+   *                                 in the source option.
+   * @param {boolean=} [highlightMatchedText=true] Flag indicating that the matched text will be highlighted in the
+   *                                               suggestions list.
+   * @param {number=} [maxResultsToShow=10] Maximum number of results to be displayed at a time.
+   * @param {boolean=} [loadOnDownArrow=false] Flag indicating that the source option will be evaluated when the down arrow
+   *                                           key is pressed and the suggestion list is closed. The current input value
+   *                                           is available as $query.
+   * @param {boolean=} {loadOnEmpty=false} Flag indicating that the source option will be evaluated when the input content
+   *                                       becomes empty. The $query variable will be passed to the expression as an empty string.
+   * @param {boolean=} {loadOnFocus=false} Flag indicating that the source option will be evaluated when the input element
+   *                                       gains focus. The current input value is available as $query.
+   * @param {boolean=} [selectFirstMatch=true] Flag indicating that the first match will be automatically selected once
+   *                                           the suggestion list is shown.
+   * @param {string=} [template=] URL or id of a custom template for rendering each element of the autocomplete list.
+   */
+  tagsInput.directive('autoComplete', ["$document","$timeout","$sce","$q","tagsInputConfig","tiUtil", function($document, $timeout, $sce, $q, tagsInputConfig, tiUtil) {
+    function SuggestionList(loadFn, options, events) {
+      var self = {}, getDifference, lastPromise, getTagId;
+
+      getTagId = function() {
+        return options.tagsInput.keyProperty || options.tagsInput.displayProperty;
+      };
+
+      getDifference = function(array1, array2) {
+        return array1.filter(function(item) {
+          return !tiUtil.findInObjectArray(array2, item, getTagId(), function(a, b) {
+            if (options.tagsInput.replaceSpacesWithDashes) {
+              a = tiUtil.replaceSpacesWithDashes(a);
+              b = tiUtil.replaceSpacesWithDashes(b);
+            }
+            return tiUtil.defaultComparer(a, b);
+          });
+        });
+      };
+
+      self.reset = function() {
+        lastPromise = null;
+
+        self.items = [];
+        self.visible = false;
+        self.index = -1;
+        self.selected = null;
+        self.query = null;
+      };
+      self.show = function() {
+        if (options.selectFirstMatch) {
+          self.select(0);
+        }
+        else {
+          self.selected = null;
+        }
+        self.visible = true;
+      };
+      self.load = tiUtil.debounce(function(query, tags) {
+        self.query = query;
+
+        var promise = $q.when(loadFn({ $query: query }));
+        lastPromise = promise;
+
+        promise.then(function(items) {
+          if (promise !== lastPromise) {
+            return;
+          }
+
+          items = tiUtil.makeObjectArray(items.data || items, getTagId());
+          items = getDifference(items, tags);
+          self.items = items.slice(0, options.maxResultsToShow);
+
+          if (self.items.length > 0) {
+            self.show();
+          }
+          else {
+            self.reset();
+          }
+        });
+      }, options.debounceDelay);
+
+      self.selectNext = function() {
+        self.select(++self.index);
+      };
+      self.selectPrior = function() {
+        self.select(--self.index);
+      };
+      self.select = function(index) {
+        if (index < 0) {
+          index = self.items.length - 1;
+        }
+        else if (index >= self.items.length) {
+          index = 0;
+        }
+        self.index = index;
+        self.selected = self.items[index];
+        events.trigger('suggestion-selected', index);
+      };
+
+      self.reset();
+
+      return self;
+    }
+
+    function scrollToElement(root, index) {
+      var element = root.find('li').eq(index),
+          parent = element.parent(),
+          elementTop = element.prop('offsetTop'),
+          elementHeight = element.prop('offsetHeight'),
+          parentHeight = parent.prop('clientHeight'),
+          parentScrollTop = parent.prop('scrollTop');
+
+      if (elementTop < parentScrollTop) {
+        parent.prop('scrollTop', elementTop);
+      }
+      else if (elementTop + elementHeight > parentHeight + parentScrollTop) {
+        parent.prop('scrollTop', elementTop + elementHeight - parentHeight);
+      }
+    }
+
+    return {
+      restrict: 'E',
+      require: '^tagsInput',
+      scope: { source: '&' },
+      templateUrl: 'ngTagsInput/auto-complete.html',
+      controller: ["$scope","$element","$attrs", function($scope, $element, $attrs) {
+        $scope.events = tiUtil.simplePubSub();
+
+        tagsInputConfig.load('autoComplete', $scope, $attrs, {
+          template: [String, 'ngTagsInput/auto-complete-match.html'],
+          debounceDelay: [Number, 100],
+          minLength: [Number, 3],
+          highlightMatchedText: [Boolean, true],
+          maxResultsToShow: [Number, 10],
+          loadOnDownArrow: [Boolean, false],
+          loadOnEmpty: [Boolean, false],
+          loadOnFocus: [Boolean, false],
+          selectFirstMatch: [Boolean, true],
+          displayProperty: [String, '']
+        });
+
+        $scope.suggestionList = new SuggestionList($scope.source, $scope.options, $scope.events);
+
+        this.registerAutocompleteMatch = function() {
+          return {
+            getOptions: function() {
+              return $scope.options;
+            },
+            getQuery: function() {
+              return $scope.suggestionList.query;
+            }
+          };
+        };
+      }],
+      link: function(scope, element, attrs, tagsInputCtrl) {
+        var hotkeys = [KEYS.enter, KEYS.tab, KEYS.escape, KEYS.up, KEYS.down],
+            suggestionList = scope.suggestionList,
+            tagsInput = tagsInputCtrl.registerAutocomplete(),
+            options = scope.options,
+            events = scope.events,
+            shouldLoadSuggestions;
+
+        options.tagsInput = tagsInput.getOptions();
+
+        shouldLoadSuggestions = function(value) {
+          return value && value.length >= options.minLength || !value && options.loadOnEmpty;
+        };
+
+        scope.addSuggestionByIndex = function(index) {
+          suggestionList.select(index);
+          scope.addSuggestion();
+        };
+
+        scope.addSuggestion = function() {
+          var added = false;
+
+          if (suggestionList.selected) {
+            tagsInput.addTag(angular.copy(suggestionList.selected));
+            suggestionList.reset();
+            tagsInput.focusInput();
+
+            added = true;
+          }
+          return added;
+        };
+
+        scope.track = function(item) {
+          return item[options.tagsInput.keyProperty || options.tagsInput.displayProperty];
+        };
+
+        tagsInput
+            .on('tag-added invalid-tag input-blur', function() {
+              suggestionList.reset();
+            })
+            .on('input-change', function(value) {
+              if (shouldLoadSuggestions(value)) {
+                suggestionList.load(value, tagsInput.getTags());
+              }
+              else {
+                suggestionList.reset();
+              }
+            })
+            .on('input-focus', function() {
+              var value = tagsInput.getCurrentTagText();
+              if (options.loadOnFocus && shouldLoadSuggestions(value)) {
+                suggestionList.load(value, tagsInput.getTags());
+              }
+            })
+            .on('input-keydown', function(event) {
+              var key = event.keyCode,
+                  handled = false;
+
+              if (hotkeys.indexOf(key) === -1) {
+                return;
+              }
+
+              if (suggestionList.visible) {
+
+                if (key === KEYS.down) {
+                  suggestionList.selectNext();
+                  handled = true;
+                }
+                else if (key === KEYS.up) {
+                  suggestionList.selectPrior();
+                  handled = true;
+                }
+                else if (key === KEYS.escape) {
+                  suggestionList.reset();
+                  handled = true;
+                }
+                else if (key === KEYS.enter || key === KEYS.tab) {
+                  handled = scope.addSuggestion();
+                }
+              }
+              else {
+                if (key === KEYS.down && scope.options.loadOnDownArrow) {
+                  suggestionList.load(tagsInput.getCurrentTagText(), tagsInput.getTags());
+                  handled = true;
+                }
+              }
+
+              if (handled) {
+                event.preventDefault();
+                event.stopImmediatePropagation();
+                return false;
+              }
+            });
+
+        events.on('suggestion-selected', function(index) {
+          scrollToElement(element, index);
+        });
+      }
+    };
+  }]);
+
+
+  /**
+   * @ngdoc directive
+   * @name tiAutocompleteMatch
+   * @module ngTagsInput
+   *
+   * @description
+   * Represents an autocomplete match. Used internally by the autoComplete directive.
+   */
+  tagsInput.directive('tiAutocompleteMatch', ["$sce","tiUtil", function($sce, tiUtil) {
+    return {
+      restrict: 'E',
+      require: '^autoComplete',
+      template: '<ng-include src="$$template"></ng-include>',
+      scope: { data: '=' },
+      link: function(scope, element, attrs, autoCompleteCtrl) {
+        var autoComplete = autoCompleteCtrl.registerAutocompleteMatch(),
+            options = autoComplete.getOptions();
+
+        scope.$$template = options.template;
+        scope.$index = scope.$parent.$index;
+
+        scope.$highlight = function(text) {
+          if (options.highlightMatchedText) {
+            text = tiUtil.safeHighlight(text, autoComplete.getQuery());
+          }
+          return $sce.trustAsHtml(text);
+        };
+        scope.$getDisplayText =  function() {
+          return tiUtil.safeToString(scope.data[options.displayProperty || options.tagsInput.displayProperty]);
+        };
+      }
+    };
+  }]);
+
+
+  /**
+   * @ngdoc directive
+   * @name tiTranscludeAppend
+   * @module ngTagsInput
+   *
+   * @description
+   * Re-creates the old behavior of ng-transclude. Used internally by tagsInput directive.
+   */
+  tagsInput.directive('tiTranscludeAppend', function() {
+    return function(scope, element, attrs, ctrl, transcludeFn) {
+      transcludeFn(function(clone) {
+        element.append(clone);
+      });
+    };
+  });
+
+  /**
+   * @ngdoc directive
+   * @name tiAutosize
+   * @module ngTagsInput
+   *
+   * @description
+   * Automatically sets the input's width so its content is always visible. Used internally by tagsInput directive.
+   */
+  tagsInput.directive('tiAutosize', ["tagsInputConfig", function(tagsInputConfig) {
+    return {
+      restrict: 'A',
+      require: 'ngModel',
+      link: function(scope, element, attrs, ctrl) {
+        var threshold = tagsInputConfig.getTextAutosizeThreshold(),
+            span, resize;
+
+        span = angular.element('<span class="input"></span>');
+        span.css('display', 'none')
+            .css('visibility', 'hidden')
+            .css('width', 'auto')
+            .css('white-space', 'pre');
+
+        element.parent().append(span);
+
+        resize = function(originalValue) {
+          var value = originalValue, width;
+
+          if (angular.isString(value) && value.length === 0) {
+            value = attrs.placeholder;
+          }
+
+          if (value) {
+            span.text(value);
+            span.css('display', '');
+            width = span.prop('offsetWidth');
+            span.css('display', 'none');
+          }
+
+          element.css('width', width ? width + threshold + 'px' : '');
+
+          return originalValue;
+        };
+
+        ctrl.$parsers.unshift(resize);
+        ctrl.$formatters.unshift(resize);
+
+        attrs.$observe('placeholder', function(value) {
+          if (!ctrl.$modelValue) {
+            resize(value);
+          }
+        });
+      }
+    };
+  }]);
+
+  /**
+   * @ngdoc directive
+   * @name tiBindAttrs
+   * @module ngTagsInput
+   *
+   * @description
+   * Binds attributes to expressions. Used internally by tagsInput directive.
+   */
+  tagsInput.directive('tiBindAttrs', function() {
+    return function(scope, element, attrs) {
+      scope.$watch(attrs.tiBindAttrs, function(value) {
+        angular.forEach(value, function(value, key) {
+          attrs.$set(key, value);
+        });
+      }, true);
+    };
+  });
+
+  /**
+   * @ngdoc service
+   * @name tagsInputConfig
+   * @module ngTagsInput
+   *
+   * @description
+   * Sets global configuration settings for both tagsInput and autoComplete directives. It's also used internally to parse and
+   * initialize options from HTML attributes.
+   */
+  tagsInput.provider('tagsInputConfig', function() {
+    var globalDefaults = {},
+        interpolationStatus = {},
+        autosizeThreshold = 3;
+
+    /**
+     * @ngdoc method
+     * @name setDefaults
+     * @description Sets the default configuration option for a directive.
+     * @methodOf tagsInputConfig
+     *
+     * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'.
+     * @param {object} defaults Object containing options and their values.
+     *
+     * @returns {object} The service itself for chaining purposes.
+     */
+    this.setDefaults = function(directive, defaults) {
+      globalDefaults[directive] = defaults;
+      return this;
+    };
+
+    /***
+     * @ngdoc method
+     * @name setActiveInterpolation
+     * @description Sets active interpolation for a set of options.
+     * @methodOf tagsInputConfig
+     *
+     * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'.
+     * @param {object} options Object containing which options should have interpolation turned on at all times.
+     *
+     * @returns {object} The service itself for chaining purposes.
+     */
+    this.setActiveInterpolation = function(directive, options) {
+      interpolationStatus[directive] = options;
+      return this;
+    };
+
+    /***
+     * @ngdoc method
+     * @name setTextAutosizeThreshold
+     * @description Sets the threshold used by the tagsInput directive to re-size the inner input field element based on its contents.
+     * @methodOf tagsInputConfig
+     *
+     * @param {number} threshold Threshold value, in pixels.
+     *
+     * @returns {object} The service itself for chaining purposes.
+     */
+    this.setTextAutosizeThreshold = function(threshold) {
+      autosizeThreshold = threshold;
+      return this;
+    };
+
+    this.$get = ["$interpolate", function($interpolate) {
+      var converters = {};
+      converters[String] = function(value) { return value; };
+      converters[Number] = function(value) { return parseInt(value, 10); };
+      converters[Boolean] = function(value) { return value.toLowerCase() === 'true'; };
+      converters[RegExp] = function(value) { return new RegExp(value); };
+
+      return {
+        load: function(directive, scope, attrs, options) {
+          var defaultValidator = function() { return true; };
+
+          scope.options = {};
+
+          angular.forEach(options, function(value, key) {
+            var type, localDefault, validator, converter, getDefault, updateValue;
+
+            type = value[0];
+            localDefault = value[1];
+            validator = value[2] || defaultValidator;
+            converter = converters[type];
+
+            getDefault = function() {
+              var globalValue = globalDefaults[directive] && globalDefaults[directive][key];
+              return angular.isDefined(globalValue) ? globalValue : localDefault;
+            };
+
+            updateValue = function(value) {
+              scope.options[key] = value && validator(value) ? converter(value) : getDefault();
+            };
+
+            if (interpolationStatus[directive] && interpolationStatus[directive][key]) {
+              attrs.$observe(key, function(value) {
+                updateValue(value);
+                scope.events.trigger('option-change', { name: key, newValue: value });
+              });
+            }
+            else {
+              updateValue(attrs[key] && $interpolate(attrs[key])(scope.$parent));
+            }
+          });
+        },
+        getTextAutosizeThreshold: function() {
+          return autosizeThreshold;
+        }
+      };
+    }];
+  });
+
+
+  /***
+   * @ngdoc factory
+   * @name tiUtil
+   * @module ngTagsInput
+   *
+   * @description
+   * Helper methods used internally by the directive. Should not be called directly from user code.
+   */
+  tagsInput.factory('tiUtil', ["$timeout", function($timeout) {
+    var self = {};
+
+    self.debounce = function(fn, delay) {
+      var timeoutId;
+      return function() {
+        var args = arguments;
+        $timeout.cancel(timeoutId);
+        timeoutId = $timeout(function() { fn.apply(null, args); }, delay);
+      };
+    };
+
+    self.makeObjectArray = function(array, key) {
+      array = array || [];
+      if (array.length > 0 && !angular.isObject(array[0])) {
+        array.forEach(function(item, index) {
+          array[index] = {};
+          array[index][key] = item;
+        });
+      }
+      return array;
+    };
+
+    self.findInObjectArray = function(array, obj, key, comparer) {
+      var item = null;
+      comparer = comparer || self.defaultComparer;
+
+      array.some(function(element) {
+        if (comparer(element[key], obj[key])) {
+          item = element;
+          return true;
+        }
+      });
+
+      return item;
+    };
+
+    self.defaultComparer = function(a, b) {
+      // I'm aware of the internationalization issues regarding toLowerCase()
+      // but I couldn't come up with a better solution right now
+      return self.safeToString(a).toLowerCase() === self.safeToString(b).toLowerCase();
+    };
+
+    self.safeHighlight = function(str, value) {
+      if (!value) {
+        return str;
+      }
+
+      function escapeRegexChars(str) {
+        return str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
+      }
+
+      str = self.encodeHTML(str);
+      value = self.encodeHTML(value);
+
+      var expression = new RegExp('&[^;]+;|' + escapeRegexChars(value), 'gi');
+      return str.replace(expression, function(match) {
+        return match.toLowerCase() === value.toLowerCase() ? '<em>' + match + '</em>' : match;
+      });
+    };
+
+    self.safeToString = function(value) {
+      return angular.isUndefined(value) || value == null ? '' : value.toString().trim();
+    };
+
+    self.encodeHTML = function(value) {
+      return self.safeToString(value)
+          .replace(/&/g, '&amp;')
+          .replace(/</g, '&lt;')
+          .replace(/>/g, '&gt;');
+    };
+
+    self.handleUndefinedResult = function(fn, valueIfUndefined) {
+      return function() {
+        var result = fn.apply(null, arguments);
+        return angular.isUndefined(result) ? valueIfUndefined : result;
+      };
+    };
+
+    self.replaceSpacesWithDashes = function(str) {
+      return self.safeToString(str).replace(/\s/g, '-');
+    };
+
+    self.simplePubSub = function() {
+      var events = {};
+      return {
+        on: function(names, handler) {
+          names.split(' ').forEach(function(name) {
+            if (!events[name]) {
+              events[name] = [];
+            }
+            events[name].push(handler);
+          });
+          return this;
+        },
+        trigger: function(name, args) {
+          var handlers = events[name] || [];
+          handlers.every(function(handler) {
+            return self.handleUndefinedResult(handler, true)(args);
+          });
+          return this;
+        }
+      };
+    };
+
+    return self;
+  }]);
+
+  /* HTML templates */
+  tagsInput.run(["$templateCache", function($templateCache) {
+    $templateCache.put('ngTagsInput/tags-input.html',
+        "<div class=\"host\" tabindex=\"-1\" ng-click=\"eventHandlers.host.click()\" ti-transclude-append=\"\">" +
+        "<div class=\"tags\" ng-class=\"{focused: hasFocus}\"><ul class=\"tag-list\">" +
+        "<li class=\"tag-item\" ng-repeat=\"tag in tagList.items track by track(tag)\" ng-class=\"{ selected: tag == tagList.selected }\">" +
+        "<ti-tag-item data=\"tag\"></ti-tag-item></li></ul>" +
+        "<input autofocus class=\"input\" autocomplete=\"off\" ng-model=\"newTag.text\" ng-change=\"eventHandlers.input.change(newTag.text)\" ng-keydown=\"eventHandlers.input.keydown($event)\" ng-focus=\"eventHandlers.input.focus($event)\" ng-blur=\"eventHandlers.input.blur($event)\" ng-paste=\"eventHandlers.input.paste($event)\" ng-trim=\"false\" ng-class=\"{'invalid-tag': newTag.invalid}\" ng-disabled=\"disabled\" ti-bind-attrs=\"{type: options.type, placeholder: options.placeholder, tabindex: options.tabindex, spellcheck: options.spellcheck}\" ti-autosize=\"\"></div></div>"
+    );
+
+    $templateCache.put('ngTagsInput/tag-item.html',
+        "<strong ng-bind=\"getDisplayLabel()\"></strong> " +
+        "<span ng-bind=\"$getDisplayText()\"></span> " +
+        "<a class=\"remove-button\" ng-click=\"$removeTag()\" ng-bind=\"$$removeTagSymbol\"></a>"
+    );
+
+    $templateCache.put('ngTagsInput/auto-complete.html',
+        "<div class=\"autocomplete\" ng-if=\"suggestionList.visible\"><ul class=\"suggestion-list\"><li class=\"suggestion-item\" ng-repeat=\"item in suggestionList.items track by track(item)\" ng-class=\"{selected: item == suggestionList.selected}\" ng-click=\"addSuggestionByIndex($index)\" ng-mouseenter=\"suggestionList.select($index)\"><ti-autocomplete-match data=\"item\"></ti-autocomplete-match></li></ul></div>"
+    );
+
+    $templateCache.put('ngTagsInput/auto-complete-match.html',
+        "<span ng-bind-html=\"$highlight($getDisplayText())\"></span>"
+    );
+  }]);
+
+}());
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/86180d93/falcon-ui/app/js/directives/server-messages.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/directives/server-messages.js b/falcon-ui/app/js/directives/server-messages.js
index e5aa58a..6bd6ea9 100644
--- a/falcon-ui/app/js/directives/server-messages.js
+++ b/falcon-ui/app/js/directives/server-messages.js
@@ -24,7 +24,12 @@
 		return {
 			replace:false,
 			restrict: 'E',
-			templateUrl: 'html/directives/serverMessagesDv.html'
+			templateUrl: 'html/directives/serverMessagesDv.html',
+      link: function (scope, element) {
+
+        //scope.allMessages
+
+      }
 		};
 	});
 	  

http://git-wip-us.apache.org/repos/asf/falcon/blob/86180d93/falcon-ui/app/js/directives/tooltip.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/directives/tooltip.js b/falcon-ui/app/js/directives/tooltip.js
new file mode 100644
index 0000000..31bb684
--- /dev/null
+++ b/falcon-ui/app/js/directives/tooltip.js
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+
+  var module = angular.module('tooltip', []);
+
+  module.directive('toogle', function () {
+    return {
+      restrict: 'A',
+      link: function(scope, element, attrs){
+        if (attrs.toggle=="tooltip"){
+          $(element).tooltip();
+        }
+        if (attrs.toggle=="popover"){
+          $(element).popover();
+        }
+      }
+    };
+  });
+
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/86180d93/falcon-ui/app/js/directives/validation-message.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/directives/validation-message.js b/falcon-ui/app/js/directives/validation-message.js
index 5bd575b..7530244 100644
--- a/falcon-ui/app/js/directives/validation-message.js
+++ b/falcon-ui/app/js/directives/validation-message.js
@@ -50,14 +50,11 @@
           element.parent().append(
             '<label ng-show="messageSwitcher.show" class="custom-danger validationMessageGral"></label>'
           );
-          //var t0 = performance.now();
           angular.forEach(element.parent().children(), function () {
             lastOne = lastOne + 1;
           });
           lastOne = lastOne - 1;
           stringLabel = $(element).parent().children()[lastOne];
-          //var t1 = performance.now();
-          //console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.");
         }
 
         function checkNameInList() {
@@ -144,4 +141,109 @@
     };
   }]);
 
+  directivesModule.directive('validationOptionalMessage', [function () {
+    return {
+      replace: false,
+      scope: {
+        validationOptionalMessage: "@",
+        required: "@"
+      },
+      restrict: 'A',
+      link: function (scope, element, attrs) {
+
+        var lastOne = 0,
+          valLength = element[0].value.length,
+          required = attrs.required,
+          stringLabel,
+          valid,
+          invalidPattern,
+          messageObject = angular.fromJson(scope.validationOptionalMessage);
+
+        messageObject.patternInvalid = messageObject.patternInvalid || messageObject.empty;
+
+        function getLabelElement() {
+          lastOne = 0;
+          element.parent().append(
+            '<label ng-show="messageSwitcher.show" class="custom-danger validationMessageGral"></label>'
+          );
+          angular.forEach(element.parent().children(), function () {
+            lastOne = lastOne + 1;
+          });
+          lastOne = lastOne - 1;
+          stringLabel = $(element).parent().children()[lastOne];
+        }
+
+        function prepare() {
+
+          valLength = element[0].value.length;
+          required =  attrs.required;
+          valid = element.hasClass('ng-valid');
+          invalidPattern = element.hasClass('ng-invalid-pattern');
+
+          if (valLength === 0 && required) {
+            element.addClass('empty');
+            angular.element(stringLabel).html(messageObject.empty).addClass('hidden');
+            element.parent().removeClass("showMessage showValidationStyle validationMessageParent");
+
+          } else if (valLength === 0 && !required) {
+            element.addClass('empty');
+            element.parent().removeClass("showMessage showValidationStyle validationMessageParent");
+            angular.element(stringLabel).addClass('hidden');
+
+          } else if (invalidPattern && valLength > 0) {
+            element.removeClass('empty');
+            angular.element(stringLabel).html(messageObject.patternInvalid).removeClass('hidden');
+            element.parent().addClass("showMessage showValidationStyle validationMessageParent");
+
+          } else if (valid && valLength > 0) {
+            element.removeClass('empty');
+            angular.element(stringLabel).addClass('hidden');
+            element.parent().removeClass("showMessage showValidationStyle validationMessageParent");
+
+          } else {
+            console.log("else");
+          }
+        }
+        function addListeners() {
+
+          element.bind('keyup', prepare);
+          element.bind('blur', function () {
+            if (valLength === 0 && required) {
+              element.removeClass('empty');
+              angular.element(stringLabel).html(messageObject.empty).removeClass('hidden');
+              element.parent().addClass("showMessage showValidationStyle validationMessageParent");
+            }
+          });
+        }
+        function normalize() {
+          prepare();
+          setTimeout(function () {
+            if (valLength === 0 && required) {
+              angular.element(stringLabel).removeClass('hidden');
+              element.removeClass('empty');
+            }
+          }, 100);
+        }
+        function init() {
+          getLabelElement();
+          addListeners();
+          prepare();
+        }
+        init();
+
+        scope.$watch(function () {
+          return scope.required;
+        }, normalize);
+
+        scope.$watch(function () {
+          return element[0].value.length;
+        }, function () {
+          if (element[0].value.length === 0) {
+            element.addClass('empty');
+          }
+        });
+      }
+    };
+  }]);
+
 }());
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/86180d93/falcon-ui/app/js/lib/bootstrap.notify.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/lib/bootstrap.notify.js b/falcon-ui/app/js/lib/bootstrap.notify.js
new file mode 100644
index 0000000..3a2fc5d
--- /dev/null
+++ b/falcon-ui/app/js/lib/bootstrap.notify.js
@@ -0,0 +1,347 @@
+/*
+ * Project: Bootstrap Notify = v3.0.2
+ * Description: Turns standard Bootstrap alerts into "Growl-like" notifications.
+ * Author: Mouse0270 aka Robert McIntosh
+ * License: MIT License
+ * Website: https://github.com/mouse0270/bootstrap-growl
+ */
+(function (factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['jquery'], factory);
+  } else if (typeof exports === 'object') {
+    // Node/CommonJS
+    factory(require('jquery'));
+  } else {
+    // Browser globals
+    factory(jQuery);
+  }
+}(function ($) {
+  // Create the defaults once
+  var defaults = {
+    element: 'body',
+    position: null,
+    type: "info",
+    allow_dismiss: true,
+    newest_on_top: false,
+    showProgressbar: false,
+    placement: {
+      from: "top",
+      align: "right"
+    },
+    offset: 20,
+    spacing: 10,
+    z_index: 1031,
+    delay: 5000,
+    timer: 1000,
+    url_target: '_blank',
+    mouse_over: null,
+    animate: {
+      enter: 'animated fadeInDown',
+      exit: 'animated fadeOutUp'
+    },
+    onShow: null,
+    onShown: null,
+    onClose: null,
+    onClosed: null,
+    icon_type: 'class',
+    template: '<div data-notify="container" class="col-xs-11 col-sm-4 alert alert-{0}" role="alert"><button type="button" aria-hidden="true" class="close" data-notify="dismiss">&times;</button><span data-notify="icon"></span> <span data-notify="title">{1}</span> <span data-notify="message">{2}</span><div class="progress" data-notify="progressbar"><div class="progress-bar progress-bar-{0}" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"></div></div><a href="{3}" target="{4}" data-notify="url"></a></div>'
+  };
+
+  String.format = function() {
+    var str = arguments[0];
+    for (var i = 1; i < arguments.length; i++) {
+      str = str.replace(RegExp("\\{" + (i - 1) + "\\}", "gm"), arguments[i]);
+    }
+    return str;
+  };
+
+  function Notify ( element, content, options ) {
+    // Setup Content of Notify
+    var content = {
+      content: {
+        message: typeof content == 'object' ? content.message : content,
+        title: content.title ? content.title : '',
+        icon: content.icon ? content.icon : '',
+        url: content.url ? content.url : '#',
+        target: content.target ? content.target : '-'
+      }
+    };
+
+    options = $.extend(true, {}, content, options);
+    this.settings = $.extend(true, {}, defaults, options);
+    this._defaults = defaults;
+    if (this.settings.content.target == "-") {
+      this.settings.content.target = this.settings.url_target;
+    }
+    this.animations = {
+      start: 'webkitAnimationStart oanimationstart MSAnimationStart animationstart',
+      end: 'webkitAnimationEnd oanimationend MSAnimationEnd animationend'
+    }
+
+    if (typeof this.settings.offset == 'number') {
+      this.settings.offset = {
+        x: this.settings.offset,
+        y: this.settings.offset
+      };
+    }
+
+    this.init();
+  };
+
+  $.extend(Notify.prototype, {
+    init: function () {
+      var self = this;
+
+      this.buildNotify();
+      if (this.settings.content.icon) {
+        this.setIcon();
+      }
+      if (this.settings.content.url != "#") {
+        this.styleURL();
+      }
+      this.placement();
+      this.bind();
+
+      this.notify = {
+        $ele: this.$ele,
+        update: function(command, update) {
+          var commands = {};
+          if (typeof command == "string") {
+            commands[command] = update;
+          }else{
+            commands = command;
+          }
+          for (var command in commands) {
+            switch (command) {
+              case "type":
+                this.$ele.removeClass('alert-' + self.settings.type);
+                this.$ele.find('[data-notify="progressbar"] > .progress-bar').removeClass('progress-bar-' + self.settings.type);
+                self.settings.type = commands[command];
+                this.$ele.addClass('alert-' + commands[command]).find('[data-notify="progressbar"] > .progress-bar').addClass('progress-bar-' + commands[command]);
+                break;
+              case "icon":
+                var $icon = this.$ele.find('[data-notify="icon"]');
+                if (self.settings.icon_type.toLowerCase() == 'class') {
+                  $icon.removeClass(self.settings.content.icon).addClass(commands[command]);
+                }else{
+                  if (!$icon.is('img')) {
+                    $icon.find('img');
+                  }
+                  $icon.attr('src', commands[command]);
+                }
+                break;
+              case "progress":
+                var newDelay = self.settings.delay - (self.settings.delay * (commands[command] / 100));
+                this.$ele.data('notify-delay', newDelay);
+                this.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', commands[command]).css('width', commands[command] + '%');
+                break;
+              case "url":
+                this.$ele.find('[data-notify="url"]').attr('href', commands[command]);
+                break;
+              case "target":
+                this.$ele.find('[data-notify="url"]').attr('target', commands[command]);
+                break;
+              default:
+                this.$ele.find('[data-notify="' + command +'"]').html(commands[command]);
+            };
+          }
+          var posX = this.$ele.outerHeight() + parseInt(self.settings.spacing) + parseInt(self.settings.offset.y);
+          self.reposition(posX);
+        },
+        close: function() {
+          self.close();
+        }
+      };
+    },
+    buildNotify: function () {
+      var content = this.settings.content;
+      this.$ele = $(String.format(this.settings.template, this.settings.type, content.title, content.message, content.url, content.target));
+      this.$ele.attr('data-notify-position', this.settings.placement.from + '-' + this.settings.placement.align);
+      if (!this.settings.allow_dismiss) {
+        this.$ele.find('[data-notify="dismiss"]').css('display', 'none');
+      }
+      if ((this.settings.delay <= 0 && !this.settings.showProgressbar) || !this.settings.showProgressbar) {
+        this.$ele.find('[data-notify="progressbar"]').remove();
+      }
+    },
+    setIcon: function() {
+      if (this.settings.icon_type.toLowerCase() == 'class') {
+        this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon);
+      }else{
+        if (this.$ele.find('[data-notify="icon"]').is('img')) {
+          this.$ele.find('[data-notify="icon"]').attr('src', this.settings.content.icon);
+        }else{
+          this.$ele.find('[data-notify="icon"]').append('<img src="'+this.settings.content.icon+'" alt="Notify Icon" />');
+        }
+      }
+    },
+    styleURL: function() {
+      this.$ele.find('[data-notify="url"]').css({
+        backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)',
+        height: '100%',
+        left: '0px',
+        position: 'absolute',
+        top: '0px',
+        width: '100%',
+        zIndex: this.settings.z_index + 1
+      });
+      this.$ele.find('[data-notify="dismiss"]').css({
+        position: 'absolute',
+        right: '10px',
+        top: '5px',
+        zIndex: this.settings.z_index + 2
+      });
+    },
+    placement: function() {
+      var self = this,
+          offsetAmt = this.settings.offset.y,
+          css = {
+            display: 'inline-block',
+            margin: '0px auto',
+            position: this.settings.position ?  this.settings.position : (this.settings.element === 'body' ? 'fixed' : 'absolute'),
+            transition: 'all .5s ease-in-out',
+            zIndex: this.settings.z_index
+          },
+          hasAnimation = false,
+          settings = this.settings;
+
+      $('[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])').each(function() {
+        return offsetAmt = Math.max(offsetAmt, parseInt($(this).css(settings.placement.from)) +  parseInt($(this).outerHeight()) +  parseInt(settings.spacing));
+      });
+      if (this.settings.newest_on_top == true) {
+        offsetAmt = this.settings.offset.y;
+      }
+      css[this.settings.placement.from] = offsetAmt+'px';
+
+      switch (this.settings.placement.align) {
+        case "left":
+        case "right":
+          css[this.settings.placement.align] = this.settings.offset.x+'px';
+          break;
+        case "center":
+          css.left = 0;
+          css.right = 0;
+          break;
+      }
+      this.$ele.css(css).addClass(this.settings.animate.enter);
+
+      $(this.settings.element).append(this.$ele);
+
+      if (this.settings.newest_on_top == true) {
+        offsetAmt = (parseInt(offsetAmt)+parseInt(this.settings.spacing)) + this.$ele.outerHeight();
+        this.reposition(offsetAmt);
+      }
+
+      if ($.isFunction(self.settings.onShow)) {
+        self.settings.onShow.call(this.$ele);
+      }
+
+      this.$ele.one(this.animations.start, function(event) {
+        hasAnimation = true;
+      }).one(this.animations.end, function(event) {
+        if ($.isFunction(self.settings.onShown)) {
+          self.settings.onShown.call(this);
+        }
+      });
+
+      setTimeout(function() {
+        if (!hasAnimation) {
+          if ($.isFunction(self.settings.onShown)) {
+            self.settings.onShown.call(this);
+          }
+        }
+      }, 600);
+    },
+    bind: function() {
+      var self = this;
+
+      this.$ele.find('[data-notify="dismiss"]').on('click', function() {
+        self.close();
+      })
+
+      this.$ele.mouseover(function(e) {
+        $(this).data('data-hover', "true");
+      }).mouseout(function(e) {
+        $(this).data('data-hover', "false");
+      });
+      this.$ele.data('data-hover', "false");
+
+      if (this.settings.delay > 0) {
+        self.$ele.data('notify-delay', self.settings.delay);
+        var timer = setInterval(function() {
+          var delay = parseInt(self.$ele.data('notify-delay')) - self.settings.timer;
+          if ((self.$ele.data('data-hover') === 'false' && self.settings.mouse_over == "pause") || self.settings.mouse_over != "pause") {
+            var percent = ((self.settings.delay - delay) / self.settings.delay) * 100;
+            self.$ele.data('notify-delay', delay);
+            self.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', percent).css('width', percent + '%');
+          }
+          if (delay <= -(self.settings.timer)) {
+            clearInterval(timer);
+            self.close();
+          }
+        }, self.settings.timer);
+      }
+    },
+    close: function() {
+      var self = this,
+          $successors = null,
+          posX = parseInt(this.$ele.css(this.settings.placement.from)),
+          hasAnimation = false;
+
+      this.$ele.data('closing', 'true').addClass(this.settings.animate.exit);
+      self.reposition(posX);
+
+      if ($.isFunction(self.settings.onClose)) {
+        self.settings.onClose.call(this.$ele);
+      }
+
+      this.$ele.one(this.animations.start, function(event) {
+        hasAnimation = true;
+      }).one(this.animations.end, function(event) {
+        $(this).remove();
+        if ($.isFunction(self.settings.onClosed)) {
+          self.settings.onClosed.call(this);
+        }
+      });
+
+      setTimeout(function() {
+        if (!hasAnimation) {
+          self.$ele.remove();
+          if (self.settings.onClosed) {
+            self.settings.onClosed(self.$ele);
+          }
+        }
+      }, 600);
+    },
+    reposition: function(posX) {
+      var self = this,
+          notifies = '[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])',
+          $elements = this.$ele.nextAll(notifies);
+      if (this.settings.newest_on_top == true) {
+        $elements = this.$ele.prevAll(notifies);
+      }
+      $elements.each(function() {
+        $(this).css(self.settings.placement.from, posX);
+        posX = (parseInt(posX)+parseInt(self.settings.spacing)) + $(this).outerHeight();
+      });
+    }
+  });
+
+  $.notify = function ( content, options ) {
+    var plugin = new Notify( this, content, options );
+    return plugin.notify;
+  };
+  $.notifyDefaults = function( options ) {
+    defaults = $.extend(true, {}, defaults, options);
+    return defaults;
+  };
+  $.notifyClose = function( command ) {
+    if (typeof command === "undefined" || command == "all") {
+      $('[data-notify]').find('[data-notify="dismiss"]').trigger('click');
+    }else{
+      $('[data-notify-position="'+command+'"]').find('[data-notify="dismiss"]').trigger('click');
+    }
+  };
+
+}));
\ No newline at end of file