'use strict';

angular.module('ark-dashboard', [
  'ark-ui-bootstrap',
  'ui.sortable',
  'widgets',
  'connectors'
]);
angular.module('widgets', []);
angular.module('connectors', ['emguo.poller']);


angular.module('ark-dashboard')
  .directive('dashboard', ['$timeout', '$q', 'WidgetModel', 'WidgetDefCollection', '$modal', 'DashboardState', '$log', 'MenusModel', 'widgetsPositionService', 'widgetsService', 'dashboardLayoutsService', 'dropdownMenuService',
      function ($timeout, $q, WidgetModel, WidgetDefCollection, $modal, DashboardState, $log, MenusModel, widgetsPositionService, widgetsService, dashboardLayoutsService, dropdownMenuService) {
    return {
      restrict: 'A',
      templateUrl: function(element, attr) {
        return attr.templateUrl ? attr.templateUrl : 'src/dashboard/template/tab-dashboard.html';
      },
      scope: true,

      controller: ['$scope', '$attrs', function (scope, attrs) {

        // default options
        var defaults = {
          stringifyStorage: true,
          hideWidgetSettings: false,
          hideWidgetClose: false,
          settingsModalOptions: {
            templateUrl: 'src/dashboard/template/rename-template.html',
            controller: 'renameModalCtrl'
          }
        };

        // from dashboard="options"
        scope.options = scope.$eval(attrs.dashboard);
        scope.options.dashboardLayoutsApi = scope.$eval(attrs.dashboardLayoutsApi);
        scope.layout = scope.$eval(attrs.layout);
        if (scope.layout && scope.layout.defaultDashboardWidget) {
          scope.layout.defaultDashboardWidget.layout = scope.layout;
        }

        // Deep options
        scope.options.settingsModalOptions = scope.options.settingsModalOptions || {};
        _.each(['settingsModalOptions'], function(key) {
          // Ensure it exists on scope.options
          scope.options[key] = scope.options[key] || {};
          // Set defaults
          _.defaults(scope.options[key], defaults[key]);
        });

        // Shallow options
        _.defaults(scope.options, defaults);

        var sortableDefaults = {
          stop: function () {
            scope.saveDashboard();
          },
          delay: 200,
          handle: '.widget-header'
        };
        scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {});

      }],
      link: function (scope, element) {

        var defaultWidgetActions = {};
        defaultWidgetActions[widgetsService.widgetTypes.DEFAULT] = {
          'renameWidget': function(layout, widget) {
            scope.openWidgetSettings(widget);
          },
          'cloneWidget': function(layout, widget) {
            scope.clone(widget);
          },
          'removeWidget': function(layout, widget) {
            scope.removeWidget(widget);
          }
        };

        var widgetMenus = new MenusModel(
          widgetsService.widgetsDefaultMenus,
          defaultWidgetActions,
          scope.options.customWidgetDropdownMenu,
          scope.options.customWidgetDropdownActions);

        scope.getWidgetMenu = function(widgetDirective) {
          return widgetMenus[widgetDirective] || widgetMenus[widgetsService.widgetTypes.DEFAULT];
        };

        // Save default widget config for reset
        scope.defaultWidgets = scope.options.defaultWidgets;

        //scope.widgetDefs = scope.options.widgetDefinitions;
        scope.widgetDefs = new WidgetDefCollection(scope.options.widgetDefinitions);
        var count = 1;

        // Instantiate new instance of dashboard state
        scope.dashboardState = new DashboardState(
          scope.options.storage,
          scope.options.storageId,
          scope.options.storageHash,
          scope.widgetDefs,
          scope.options.stringifyStorage
        );


        var minColumns = 1;
        var minRows = 1;
        var maxRows = 100;
        var minSizeX = 1;
        var maxSizeX = 6;
        var minSizeY = 1;
        var maxSizeY = 6;
        var defaultSizeX = 2;
        var defaultSizeY = 2;
        var resizeEnabled = true;
        var dragEnabled = true;
        var fixed = scope.layout && (scope.layout.type === dashboardLayoutsService.layoutTypes.WALLBOARD);
        var columns = (scope.layout && scope.layout.cols) || 6;
        var mobileModeDisabled = scope.layout && scope.layout.mobileModeDisabled;

        var widgetSizeLimit;
        if (scope.options.storage && scope.options.storage.options) {
          widgetSizeLimit = scope.options.storage.options.widgetSizeLimit;
          resizeEnabled = !scope.options.storage.options.resizeDisabled;
          dragEnabled = !scope.options.storage.options.dragDisabled;
        }

        if (widgetSizeLimit) {
          minRows = widgetSizeLimit;
          maxRows = 20;
          minSizeX = widgetSizeLimit;
          maxSizeX = widgetSizeLimit;
          minSizeY = widgetSizeLimit;
          maxSizeY = widgetSizeLimit;
          resizeEnabled = false;
        }

        scope.gridsterOpts = {
          columns: columns, // the width of the grid, in columns
          pushing: true, // whether to push other items out of the way on move or resize
          floating: true, // whether to automatically float items up so they stack (you can temporarily disable if you are adding unsorted items with ng-repeat)
          swapping: false, // whether or not to have items of the same size switch places instead of pushing down if they are the same size
          width: 'auto', // can be an integer or 'auto'. 'auto' scales gridster to be the full width of its containing element
          colWidth: 'auto', // can be an integer or 'auto'.  'auto' uses the pixel width of the element divided by 'columns'
          rowHeight: 200, // can be an integer or 'match'.  Match uses the colWidth, giving you square widgets.
          margins: [0, 0], // the pixel distance between each widget
          outerMargin: true, // whether margins apply to outer edges of the grid
          isMobile: true, // stacks the grid items if true
          mobileBreakPoint: 511, // if the screen is not wider that this, remove the grid layout and stack the items
          mobileModeEnabled: !fixed && !mobileModeDisabled, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint
          minColumns: minColumns, // the minimum columns the grid must have
          minRows: minRows, // the minimum height of the grid, in rows
          maxRows: maxRows,
          defaultSizeX: defaultSizeX, // the default width of a gridster item, if not specifed
          defaultSizeY: defaultSizeY, // the default height of a gridster item, if not specified
          minSizeX: minSizeX, // minimum column width of an item
          maxSizeX: maxSizeX, // maximum column width of an item
          minSizeY: minSizeY, // minumum row height of an item
          maxSizeY: maxSizeY, // maximum row height of an item
          resizable: {
            enabled: resizeEnabled,
            handles: ['n', 'e', 's', 'w', 'ne', 'se', 'sw', 'nw'],
            stop: function () {
              scope.resize();
            }
          },
          draggable: {
            enabled: dragEnabled, // whether dragging items is supported
            handle: '.widget-anchor',
            stop: function () {
              scope.drag();
            }
          },
          widgetsPositionService: !fixed && widgetsPositionService
        };

        if (fixed && scope.layout.rows) {
          var top = scope.gridsterOpts.rowHeight * scope.layout.rows;
          var text = '';
          if (scope.options.storage && scope.options.storage.options) {
            text = scope.options.storage.options.overlayText;
          }
          scope.overlayOpts = {
            style: {
                top: top,
                height: '0px'
            },
            text: text
          };
          var scrollListener;
          scope.$on('gridster-resized', function () {
            var $overlay = element.find('.dashboard-overlay');
            var $dashboard = element.find('.dashboard-area.scroll-content');
            if (scrollListener === undefined) {
              scrollListener = function() {
                $timeout(function () {
                  scope.overlayOpts.style.top = Math.max(top - $dashboard.scrollTop(), 0);
                  scope.overlayOpts.style.height = $dashboard.height() - scope.overlayOpts.style.top;
                  scope.overlayOpts.style.height += 'px';
                });
              };
              $dashboard.on("scroll", scrollListener);
            }
            if ($overlay && $overlay.length) {
              scrollListener();
            }
          });
          scope.$on('$destroy', function () {
            if (scrollListener !== undefined) {
              var $dashboard = element.find(".dashboard-area.scroll-content");
              $dashboard.off("scroll", scrollListener);
              scrollListener = undefined;
            }
          });
        }
        /**

        /**
         * Resize widget and save sizes
         * @param  {Object} widget The widget instance object (not a definition object)
         */
        scope.resize = function() {
          scope.saveDashboard(true);
        };

        /**
         * Grag widget and save sizes
         * @param  {Object} widget The widget instance object (not a definition object)
         */
        scope.drag = function() {
          scope.saveDashboard(true);
        };

        /**
         * Instantiates a new widget on the dashboard
         * @param {Object} widgetToInstantiate The definition object of the widget to be instantiated
         * @param {boolean} skipSave true if no need to call saveDashboard
         */
        scope.addWidget = function (widgetToInstantiate, skipSave) {
          var defaultWidgetDefinition = scope.widgetDefs.getByName(widgetToInstantiate.name);
          if (!defaultWidgetDefinition) {
            throw 'Widget ' + widgetToInstantiate.name + ' is not found.';
          }
          // Determine the title for the new widget
          var title;
          if (widgetToInstantiate.title) {
            title = widgetToInstantiate.title;
          } else if (defaultWidgetDefinition.title) {
            title = defaultWidgetDefinition.title;
          } else {
            title = 'Widget ' + count++;
          }

         // Determine the sizes for the new widget
          var sizeX;
          var sizeY;
          if(widgetToInstantiate.sizeX) {
            sizeX = widgetToInstantiate.sizeX;
          }
          if(widgetToInstantiate.sizeY) {
            sizeY = widgetToInstantiate.sizeY;
          }

         // Determine the row and column positions for the new widget
          var row;
          var col;
          if(widgetToInstantiate.row !== undefined) {
            row = widgetToInstantiate.row;
          } else {
            row = 0;
          }
          if(widgetToInstantiate.col !== undefined) {
            col = widgetToInstantiate.col;
          } else {
            col = 0;
          }

          // Deep extend a new object for instantiation
          widgetToInstantiate = angular.extend(widgetToInstantiate, defaultWidgetDefinition);

          // Instantiation
          var widget = new WidgetModel(widgetToInstantiate, {
            widgetGuid: widgetToInstantiate.widgetGuid,
            title: title,
            hideTitle: widgetToInstantiate.hideTitle,
            sizeX: sizeX,
            sizeY: sizeY,
            minSizeX: widgetToInstantiate.minSizeX,
            minSizeY: widgetToInstantiate.minSizeY,
            maxSizeX: widgetToInstantiate.maxSizeX,
            maxSizeY: widgetToInstantiate.maxSizeY,
            row: row,
            col: col,
            position: widgetToInstantiate.position || widgetsPositionService.POSITION_TYPES.DEFAULT,
            orderIndex: widgetToInstantiate.orderIndex,
            connectionIndicator: widgetToInstantiate.connectionIndicator || {
              supported: false,
              connected: false
            }
          });

          scope.widgets.push(widget);
          if (!skipSave) {
            scope.saveDashboard();
          }

          return widget;
        };

        /**
         * Updates a widget instance on the dashboard
         * @param  {Object} widget The object with options to be updated.
         */
        scope.updateWidget = function (widget) {
          var widgetToUpdate = _.find(scope.widgets, function (oldWidget) {
              return oldWidget.widgetGuid === widget.widgetGuid;
            });
          if (widgetToUpdate) {
            var defaultWidgetDefinition = scope.widgetDefs.getByName(widget.name);
            if (!defaultWidgetDefinition) {
              throw 'Widget ' + widget.name + ' is not found.';
            }
            var newWidget = angular.extend({}, defaultWidgetDefinition, widget),
          overrides = angular.extend({},
            _.pick(widgetToUpdate, ['widgetGuid', 'row', 'col', 'orderIndex', 'connectionIndicator', 'position']),
            _.pick(newWidget, ['sizeX', 'sizeY', 'minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'title']));
            newWidget = new WidgetModel(newWidget, overrides);
            scope.widgets.splice(scope.widgets.indexOf(widgetToUpdate), 1, newWidget);
            scope.$emit('widgetChanged', newWidget);
          }
        };

        /**
         * Removes a widget instance from the dashboard
         * @param  {Object} widget The widget instance object (not a definition object)
         * @param  {boolean} skipSave true if no need to call saveDashboard
         */
        scope.removeWidget = function (widget, skipSave) {
          scope.widgets.splice(_.indexOf(scope.widgets, widget), 1);
          var defaultWidgetToDelete = _.find(scope.defaultWidgets, function(w) {
            return w.widgetGuid === widget.widgetGuid;
          });
          scope.defaultWidgets.splice(_.indexOf(scope.defaultWidgets, defaultWidgetToDelete), 1);

          if (!skipSave) {
            scope.saveDashboard(true);
          }
        };

        /**
         * Removes a widget instance with given id from the dashboard
         * @param  {string} guid guid of widget instance object to be removed
         * @param  {boolean} skipSave true if no need to call saveDashboard
         */
        scope.removeWidgetById = function(guid, skipSave) {
          if(guid) {
            var widgetToDelete = _.find(scope.widgets, function(w) {
              return w.widgetGuid === guid;
            });
            if(widgetToDelete && !skipSave) {
              scope.removeWidget(widgetToDelete);
            }
          }
        };

        /**
         * Opens a dialog for setting and changing widget properties
         * @param  {Object} widget The widget instance object
         */
        scope.openWidgetSettings = function (widget) {

          // Set up $modal options
          var options = _.defaults(
            { scope: scope },
            widget.settingsModalOptions,
            scope.options.settingsModalOptions);

          // Ensure widget is resolved
          options.resolve = {
            title: function () {
              return widget.title;
            },
            type: function () {
              return 'Widget';
            }
          };
          var oldtitle = widget.title;

          // Create the modal
          var modalInstance = $modal.open(options);

          // Set resolve and reject callbacks for the result promise
          modalInstance.result.then(
            function (result) {
              widget.title = result;
              //AW Persist title change from options editor
              scope.$emit('widgetChanged', widget);
              //Save dashboard
              scope.saveDashboard(true);
            },
            function () {
              widget.title = oldtitle;
            }
          );

        };

        /**
         * Remove all widget instances from dashboard
         */
        scope.clear = function (skipSave) {
          scope.widgets = [];
          if (skipSave === true) {
            return;
          }
          scope.saveDashboard(true);
        };

        /**
         * Clone current widget
         * @param  {Object} widget The widget instance object
         */
        scope.clone = function (widget) {
          return $q.when(widget)
            .then(scope.options.storage && scope.options.storage.storage.createNewWidget)
            .then(scope.addWidget);
        };

        /**
         * Used for preventing default on click event
         * @param {Object} event     A click event
         * @param {Object} widgetDef A widget definition object
         */
        scope.addWidgetInternal = function (event, widgetDef) {
          event.preventDefault();
          scope.addWidget(widgetDef);
        };

        /**
         * Uses dashboardState service to save state
         */
        scope.saveDashboard = function (force) {
          var reindexOrder = function(widgets) {
            widgets.forEach(function(w) {
              if(w.position === widgetsPositionService.POSITION_TYPES.DEFAULT && w.row !== undefined && w.col !== undefined) {
                w.orderIndex = ((w.row + 1) * 100 + (w.col + 1)).toString();
              }
            });
          };
          reindexOrder(scope.widgets);
          if (!scope.options.explicitSave) {
            scope.dashboardState.save(scope.widgets);
          } else {
            if (!angular.isNumber(scope.options.unsavedChangeCount)) {
              scope.options.unsavedChangeCount = 0;
            }
            if (force) {
              scope.options.unsavedChangeCount = 0;
              scope.dashboardState.save(scope.widgets);

            } else {
              ++scope.options.unsavedChangeCount;
            }
          }
        };

        /**
         * Wraps saveDashboard for external use.
         */
        scope.externalSaveDashboard = function() {
          scope.saveDashboard(true);
        };

        /**
         * Clears current dash and instantiates widget definitions
         * @param  {Array} widgets Array of definition objects
         */
        scope.loadWidgets = function (widgets) {
          // AW dashboards are continuously saved today (no "save" button).
          //scope.defaultWidgets = widgets;
          scope.savedWidgetDefs = widgets;
          scope.clear(true);
          _.each(widgets, function (widgetDef) {
            scope.addWidget(widgetDef, true);
          });
        };

        /**
         * Resets widget instances to default config
         * @return {[type]} [description]
         */
        scope.resetWidgetsToDefault = function () {
          scope.loadWidgets(scope.defaultWidgets);
          scope.saveDashboard();
        };

        scope.getWidgets = function () {
          return scope.widgets;
        };

        scope.getGridsterWidgets = function () {
          return scope.widgets.filter(function (widget) {
            return widget.position === widgetsPositionService.POSITION_TYPES.DEFAULT;
          });
        };

        scope.hasFooterWidget = function () {
          return scope.widgets.some(function (widget) {
            return widget.position === widgetsPositionService.POSITION_TYPES.FOOTER;
          });
        };

        scope.getFooterWidgets = function () {
          return scope.widgets.filter(function (widget) {
            return widget.position === widgetsPositionService.POSITION_TYPES.FOOTER;
          });
        };

        // Set default widgets array
        var savedWidgetDefs = scope.dashboardState.load();

        // Success handler
        function handleStateLoad(saved) {
          scope.options.unsavedChangeCount = 0;
          if (saved && saved.length) {
            scope.loadWidgets(saved);
          } else if (scope.defaultWidgets) {
            scope.loadWidgets(scope.defaultWidgets);
          } else {
            scope.clear(true);
          }
        }

        var documentClickHandler = function (event) {
          var element = angular.element(this);
          angular.element('.widget-container').removeClass('gridster-item-moving-dropdown-open');
          element.closest('.widget-container').addClass('gridster-item-moving-dropdown-open');
          dropdownMenuService.changeDropdownMenuOrientation(element.next(), event.pageX);
        };

        var windowResizeHandler = function () {
          dropdownMenuService.changeActiveDropdownMenuOrientation();
        };

        angular.element(document).on('click', '.active a.nav-tabs-dropdown, .widget-menu-collapse a.nav-tabs-dropdown', documentClickHandler);
        angular.element(window).on('resize', windowResizeHandler);
        scope.$on('gridster-item-transition-end', windowResizeHandler);

        if (angular.isArray(savedWidgetDefs)) {
          handleStateLoad(savedWidgetDefs);
        } else if (savedWidgetDefs && angular.isObject(savedWidgetDefs) && angular.isFunction(savedWidgetDefs.then)) {
          savedWidgetDefs.then(handleStateLoad, handleStateLoad);
        } else {
          handleStateLoad();
        }

        // expose functionality externally
        // functions are appended to the provided dashboard options
        scope.options.addWidget = scope.addWidget;
        scope.options.loadWidgets = scope.loadWidgets;
        scope.options.saveDashboard = scope.externalSaveDashboard;
        scope.options.updateWidget = scope.updateWidget;
        scope.options.removeWidget = scope.removeWidget;
        scope.options.openWidgetSettings = scope.openWidgetSettings;
        scope.options.getWidgets = scope.getWidgets;

        // save state
        scope.$on('widgetChanged', function (event) {
          event.stopPropagation();
          scope.saveDashboard();
        });

        scope.$on('$destroy', function () {
          angular.element(document).off('click', documentClickHandler);
          angular.element(window).off('resize', windowResizeHandler);
          widgetMenus.destroy();
        });
      }
    };
  }]);

'use strict';

angular.module('ark-dashboard')
  .directive('dashboardLayouts', ['$timeout', '$modal', 'LayoutStorage', 'MenusModel', 'dashboardLayoutsService',
    function($timeout, $modal, LayoutStorage, MenusModel, dashboardLayoutsService) {
      return {
        restrict: "AE",
        scope: true,
        templateUrl: function(element, attr) {
          return attr.templateUrl ? attr.templateUrl : 'src/dashboard/template/dashboard-layouts.html';
        },
        link: function(scope, element, attrs) {

          scope.widgetLayout = true;

          scope.options = scope.$eval(attrs.layoutsOptions);
          scope.dashboardLayoutsApi = scope.$eval(attrs.layoutsApi) || {};

          var layoutStorage = new LayoutStorage(scope.options);

          scope.layouts = layoutStorage.layouts;

          var renameLayout = function(layout) {
            scope.options.renameLayout(layout);
          };
          var removeLayout = function(layout) {
            scope.options.removeLayout(layout);
          };

          var defaultLayoutDropdownActions = {};
          defaultLayoutDropdownActions[dashboardLayoutsService.layoutTypes.DASHBOARD] = {
            'renameLayout' : renameLayout,
            'removeLayout' : removeLayout
          };
          defaultLayoutDropdownActions[dashboardLayoutsService.layoutTypes.WALLBOARD] = {
            'renameLayout' : renameLayout,
            'removeLayout' : removeLayout
          };
          defaultLayoutDropdownActions[dashboardLayoutsService.layoutTypes.WIDGET] = {
            'renameLayout' : renameLayout,
            'removeLayout' : removeLayout
          };

          var layoutsMenus = new MenusModel(
            dashboardLayoutsService.layoutsDefaultMenus,
            defaultLayoutDropdownActions,
            scope.options.customLayoutDropdownMenu,
            scope.options.customLayoutDropdownActions);

          scope.$watch('$destroy', function () {
            layoutsMenus.destroy();
          });

          scope.$watch('layouts.length', function() {
            angular.forEach(scope.layouts, function(layout) {
              layout.layoutMenu = layoutsMenus[layout.type];
              if (!layout.defaultDashboardWidget && scope.options.defaultDashboardWidget) {
                layout.defaultDashboardWidget = scope.options.defaultDashboardWidget;
              }
            });
          });

          var hadCloseEver = false;
          if (scope.options.restrictMinimumLayoutsTo && scope.options.restrictMinimumLayoutsTo > 0) {
              scope.$watch('layouts.length', function(newVal, oldVal) {
                  if(newVal !== oldVal) {
                      var layouts = scope.layouts.filter(function(layout) {
                          return layout.type === dashboardLayoutsService.layoutTypes.DASHBOARD || layout.type === dashboardLayoutsService.layoutTypes.WALLBOARD;
                      });
                      if(layouts.length <= scope.options.restrictMinimumLayoutsTo) {
                          removeCloseOptionFromMenu(dashboardLayoutsService.layoutTypes.DASHBOARD);
                          removeCloseOptionFromMenu(dashboardLayoutsService.layoutTypes.WALLBOARD);
                      } else {
                          addCloseOptionToMenu(dashboardLayoutsService.layoutTypes.DASHBOARD);
                          addCloseOptionToMenu(dashboardLayoutsService.layoutTypes.WALLBOARD);
                      }
                  }

                  function hasCloseMenuItem(type) {
                      return _(layoutsMenus[type].list).some(function(item) {
                          return item.menuOptionKey === dashboardLayoutsService.layoutMenuOptions.CLOSE.menuOptionKey;
                      });
                  }

                  function removeCloseOptionFromMenu(type) {
                      if(hasCloseMenuItem(type)) {
                          hadCloseEver = true;
                          layoutsMenus[type].list = layoutsMenus[type].list.filter(function (item) {
                              return item.menuOptionKey !== dashboardLayoutsService.layoutMenuOptions.CLOSE.menuOptionKey;
                          });
                      }
                  }
                  function addCloseOptionToMenu(type) {
                      if(hadCloseEver && !hasCloseMenuItem(type)) {
                          layoutsMenus[type].list.push(dashboardLayoutsService.layoutMenuOptions.CLOSE);
                      }
                  }
              });
          }

          scope.onTabsBarAction = function(actionKey) {
            if (actionKey && scope.options.customTabsBarAction && typeof scope.options.customTabsBarAction[actionKey] === 'function') {
              scope.options.customTabsBarAction[actionKey](scope.dashboardLayoutsApi);
            }
          };

          scope.options.createNewLayout = function(layoutType, title, defaultWidgets, createAfterLayout, dashboard) {
            var layoutTitle,
                layoutIcon;

            switch(layoutType) {
              case dashboardLayoutsService.layoutTypes.DASHBOARD:
                layoutTitle = (title) ? title : 'New Dashboard';
                break;
              case dashboardLayoutsService.layoutTypes.WALLBOARD:
                layoutTitle = (title) ? title : 'New Wallboard';
                break;
              case dashboardLayoutsService.layoutTypes.WIDGET:
                layoutTitle = (title) ? title : 'Expanded Widget';
                break;
              default:
                layoutTitle = (title) ? title : layoutType;
                if (scope.options.customLayoutDropdownMenu && scope.options.customLayoutDropdownMenu[layoutType]) {
                  layoutIcon = scope.options.customLayoutDropdownMenu[layoutType].layoutIcon;
                }
            }

            var newLayout = {
              title: layoutTitle,
              type: layoutType,
              icon: layoutIcon,
              layoutMenu: layoutsMenus[layoutType],
              defaultWidgets: defaultWidgets || scope.options.defaultWidgets || []
            };

            if (dashboard) {
              newLayout = angular.extend(dashboard, newLayout);
            }

            var saveLayout = function() {
              layoutStorage.add(newLayout, createAfterLayout);
              scope._makeLayoutActive(newLayout);
              layoutStorage.save();
            };

            if(layoutStorage.storage && layoutStorage.storage.createNewTab) {
              layoutStorage.storage.createNewTab(newLayout).then(function(layout) {
                if(layout) {
                  newLayout = layout;
                }
                saveLayout();
              }, function(error) {
                console.error("Cannot save tab: "+error);
              });
            } else {
              saveLayout();
            }
            return newLayout;
          };

          //TODO: this is to dangerous that this method can remove active layout in case of no arguments specifed
          //      because by some mistake in calling code argument can be undefined or null.
          //      Suggestion is to provide separate method removeActiveLayout.
          //      And this method should throw eroror or do nothing if it's called without argumnets.
          scope.options.removeLayout = function(curlayout) {
            var layout = curlayout || layoutStorage.getActiveLayout();
            layoutStorage.remove(layout);
            layoutStorage.save();
          };

          scope.options.makeLayoutActive = function(layout) {
            var current = layoutStorage.getActiveLayout();

            if (current && current.dashboard.unsavedChangeCount) {
              var modalInstance = $modal.open({
                templateUrl: 'src/dashboard/template/save-changes-modal.html',
                resolve: {
                  layout: function() {
                    return layout;
                  }
                },
                controller: 'SaveChangesModalCtrl'
              });

              // Set resolve and reject callbacks for the result promise
              modalInstance.result.then(
                function() {
                  scope.options.saveDashboard();
                  scope._makeLayoutActive(layout);
                },
                function() {
                  scope._makeLayoutActive(layout);
                }
              );
            } else {
              scope._makeLayoutActive(layout);
            }
            scope.$broadcast('activeTabChanged');
          };

          scope._makeLayoutActive = function(layout) {
            angular.forEach(scope.layouts, function(l) {
              if (l !== layout) {
                l.active = false;
              } else {
                l.active = true;
              }
            });
            layoutStorage.save();
          };

          scope.isCached = function(layout) {
            return !(layout.nonCached ||
              layout.type === dashboardLayoutsService.layoutTypes.DASHBOARD ||
              layout.type === dashboardLayoutsService.layoutTypes.WALLBOARD ||
              layout.type === dashboardLayoutsService.layoutTypes.WIDGET);
          };

          scope.isActiveNonCached = function(layout) {
            return !!layout.active && !scope.isCached(layout);
          };

          scope.options.renameLayout = function(curlayout) {
            var layout = curlayout || layoutStorage.getActiveLayout();

            var modalInstance = $modal.open({
              templateUrl: 'src/dashboard/template/rename-template.html',
              resolve: {
                title: function() {
                  return layout.title;
                },
                type: function() {
                  return 'Dashboard';
                }
              },
              controller: 'renameModalCtrl'
            });

            var oldtitle = layout.title;

            // Set resolve and reject callbacks for the result promise
            modalInstance.result.then(
              function (result) {
                layout.title = result;
                scope._makeLayoutActive(layout);
                scope.options.saveDashboard();
              },
              function() {
                layout.title = oldtitle;
                scope._makeLayoutActive(layout);
              }
            );
          };

          // saves whatever is in the title input as the new title
          scope.options.saveTitleEdit = function(layout) {
            layout.editingTitle = false;
            layoutStorage.save();
          };

          scope.options.saveLayouts = function() {
            layoutStorage.save(true);
          };
          //add widget to active layout
          scope.options.addWidget = function() {
            var layout = layoutStorage.getActiveLayout();
            if (layout) {
              layout.dashboard.addWidget.apply(layout.dashboard, arguments);
            }
          };

          scope.options.addWidgetToLayout = function(layoutGuid, widget) {
            var layout = scope.options.getLayoutById(layoutGuid);
            layout.dashboard.addWidget(widget);
          };

          var updateWidgetInLayout = function(layout, widget) {
            if (layout.dashboard && layout.dashboard.updateWidget) {
              layout.dashboard.updateWidget(widget);
            }
          };

          scope.options.updateWidgetInLayout = function(layoutGuid, widget) {
            var layout = scope.options.getLayoutById(layoutGuid);
            if (layout) {
              updateWidgetInLayout(layout, widget);
            }
          };

          scope.options.updateWidgetInAllLayouts = function (widget) {
            scope.layouts.forEach(function (layout) {
              updateWidgetInLayout(layout, widget);
            });
          };

          scope.options.loadWidgets = function() {
            var layout = layoutStorage.getActiveLayout();
            if (layout) {
              layout.dashboard.loadWidgets.apply(layout.dashboard, arguments);
            }
          };
          scope.options.resetWidgetsToDefault = function () {
            var layout = layoutStorage.getActiveLayout();
            layout.dashboard.loadWidgets(layout.defaultWidgets);
            scope.options.saveDashboard();
          };
          scope.options.saveDashboard = function() {
            var layout = layoutStorage.getActiveLayout();
            if (layout) {
              layout.dashboard.saveDashboard.apply(layout.dashboard, arguments);
            }
          };
          scope.options.getAllLayouts = function () {
            return scope.layouts;
          };
          scope.options.getCurrentActiveLayout = function () {
            return layoutStorage.getActiveLayout();
          };
          scope.options.getLayoutById = function (layoutId) {
            return _.findWhere(scope.layouts, {id: layoutId});
          };

          angular.extend(scope.dashboardLayoutsApi, {
            createNewLayout : scope.options.createNewLayout,
            removeLayout : scope.options.removeLayout,
            makeLayoutActive : scope.options.makeLayoutActive,
            renameLayout : scope.options.renameLayout,
            saveTitleEdit : scope.options.saveTitleEdit,
            saveLayouts : scope.options.saveLayouts,
            addWidget : scope.options.addWidget,
            addWidgetToLayout: scope.options.addWidgetToLayout,
            updateWidgetInLayout: scope.options.updateWidgetInLayout,
            updateWidgetInAllLayouts: scope.options.updateWidgetInAllLayouts,
            loadWidgets : scope.options.loadWidgets,
            resetWidgetsToDefault : scope.options.resetWidgetsToDefault,
            saveDashboard : scope.options.saveDashboard,
            getAllLayouts : scope.options.getAllLayouts,
            getCurrentActiveLayout: scope.options.getCurrentActiveLayout,
            getLayoutById: scope.options.getLayoutById
          });


          scope.addTooptip = function () {
            $timeout(function(){
              var el = element.find('.tabs-title');
              for(var i = 0; i < el.length; i++) {
                if(el[i].offsetWidth < el[i].scrollWidth) {
                  scope.layouts[i].showTooltip = true;
                } else {
                  scope.layouts[i].showTooltip = false;
                }
              }
            });
          };

          var sortableDefaults = {
            stop: function() {
              scope.options.saveLayouts();
            },
            axis: 'x',
            delay: 200,
            placeholder: 'sortable-placeholder'
          };
          scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {});
        }
      };
    }
  ]);

'use strict';

/**
 *  nvd3 widget wrapper
 */

angular.module('ark-dashboard')
  .directive('widgetCompiler', ["$compile", "$timeout", function ($compile, $timeout) {
    var cacheOfCompiledDirectives = {};
    return {
      restrict: 'E',
      scope: {
        options: '='
      },
      link: function (scope, element) {
        element.empty();
        $timeout(function () {
          if (!cacheOfCompiledDirectives[scope.options.directive]) {
            var htmlSource = '<' + scope.options.directive + ' options="options" />';
            cacheOfCompiledDirectives[scope.options.directive] = $compile(htmlSource);
          }
          cacheOfCompiledDirectives[scope.options.directive](scope, function (clonedElement) {
            element.append(clonedElement);
          });
        }, 100);
      }
    };
  }]);

'use strict';

angular.module('ark-dashboard')
  .directive('widget', ['$injector', function ($injector) {

    return {

      controller: 'DashboardWidgetCtrl',

      link: function (scope) {

        var widget = scope.widget;
        var dataModelType = widget.dataModelType;

        scope.widget.isErrorViewActive = false;
        scope.widget.errorViewIndicatorClicked = function() {
            scope.widget.isErrorViewActive = !scope.widget.isErrorViewActive;
        };

        // set up data source
        if (dataModelType) {
          var DataModelConstructor; // data model constructor function

          if (angular.isFunction(dataModelType)) {
            DataModelConstructor = dataModelType;
          } else if (angular.isString(dataModelType)) {
            $injector.invoke([dataModelType, function (DataModelType) {
              DataModelConstructor = DataModelType;
            }]);
          } else {
            throw new Error('widget dataModelType should be function or string');
          }

          var ds;
          if (widget.dataModelArgs) {
            ds = new DataModelConstructor(widget.dataModelArgs);
          } else {
            ds = new DataModelConstructor();
          }
          widget.dataModel = ds;
          ds.setup(widget, scope);
          ds.init();
          scope.$on('$destroy', function () {
            if (ds.destroy) {
              ds.destroy();
            }
            scope.$emit('widgetRemoved', widget);
          });
          scope.$on('gridster-item-moving', function () {
            scope.$emit('widgetMoving', widget);
          });
        }

        scope.$emit('widgetAdded', widget);

      }

    };
  }]);

'use strict';

angular.module('ark-dashboard')
  .factory('DashboardState', ['$log', '$q', function ($log, $q) {
    function DashboardState(storage, id, hash, widgetDefinitions, stringify) {
      this.storage = storage;
      this.id = id;
      this.hash = hash;
      this.widgetDefinitions = widgetDefinitions;
      this.stringify = stringify;
    }

    DashboardState.prototype = {
      /**
       * Takes array of widget instance objects, serializes,
       * and saves state.
       *
       * @param  {Array} widgets  scope.widgets from dashboard directive
       * @return {Boolean}        true on success, false on failure
       */
      save: function (widgets) {

        if (!this.storage) {
          return true;
        }

        var serialized = _.map(widgets, function (widget) {
          var widgetObject = {
            widgetGuid: widget.widgetGuid,
            title: widget.title,
            name: widget.name,
            hideTitle: widget.hideTitle,
            style: widget.style,
            size: widget.size,
            sizeX: widget.sizeX,
            sizeY: widget.sizeY,
            minSizeX: widget.minSizeX,
            minSizeY: widget.minSizeY,
            maxSizeX: widget.maxSizeX,
            maxSizeY: widget.maxSizeY,
            col: widget.col,
            row: widget.row,
            position: widget.position,
            orderIndex: widget.orderIndex,
            dataModelOptions: widget.dataModelOptions,
            storageHash: widget.storageHash,
            attrs: widget.attrs,
            contentStyle: widget.contentStyle
          };

          return widgetObject;
        });

        var item = { widgets: serialized, hash: this.hash };

        if (this.stringify) {
          item = JSON.stringify(item);
        }

        this.storage.setItem(this.id, item);
        return true;
      },

      /**
       * Loads dashboard state from the storage object.
       * Can handle a synchronous response or a promise.
       *
       * @return {Array|Promise} Array of widget definitions or a promise
       */
      load: function () {

        if (!this.storage) {
          return null;
        }

        var serialized;

        // try loading storage item
        serialized = this.storage.getItem( this.id );

        if (serialized) {
          // check for promise
          if (angular.isObject(serialized) && angular.isFunction(serialized.then)) {
            return this._handleAsyncLoad(serialized);
          }
          // otherwise handle synchronous load
          return this._handleSyncLoad(serialized);
        } else {
          return null;
        }
      },

      _handleSyncLoad: function(serialized) {

        var deserialized, result = [];

        if (!serialized) {
          return null;
        }

        if (this.stringify) {
          try { // to deserialize the string

            deserialized = JSON.parse(serialized);

          } catch (e) {

            // bad JSON, log a warning and return
            $log.warn('Serialized dashboard state was malformed and could not be parsed: ', serialized);
            return null;

          }
        }
        else {
          deserialized = serialized;
        }

        // check hash against current hash
        if (deserialized.hash !== this.hash) {

          $log.info('Serialized dashboard from storage was stale (old hash: ' + deserialized.hash + ', new hash: ' + this.hash + ')');
          this.storage.removeItem(this.id);
          return null;

        }

        // Cache widgets
        var savedWidgetDefs = deserialized.widgets;

        // instantiate widgets from stored data
        for (var i = 0; i < savedWidgetDefs.length; i++) {

          // deserialized object
          var savedWidgetDef = savedWidgetDefs[i];

          // widget definition to use
          var widgetDefinition = this.widgetDefinitions.getByName(savedWidgetDef.name);

          // check for no widget
          if (!widgetDefinition) {
            // no widget definition found, remove and return false
            $log.warn('Widget with name "' + savedWidgetDef.name + '" was not found in given widget definition objects');
            continue;
          }

          // check widget-specific storageHash
          if (widgetDefinition.hasOwnProperty('storageHash') && widgetDefinition.storageHash !== savedWidgetDef.storageHash) {
            // widget definition was found, but storageHash was stale, removing storage
            $log.info('Widget Definition Object with name "' + savedWidgetDef.name + '" was found ' +
              'but the storageHash property on the widget definition is different from that on the ' +
              'serialized widget loaded from storage. hash from storage: "' + savedWidgetDef.storageHash + '"' +
              ', hash from WDO: "' + widgetDefinition.storageHash + '"');
            continue;
          }

          // push instantiated widget to result array
          result.push(savedWidgetDef);
        }

        return result;
      },

      _handleAsyncLoad: function(promise) {
        var self = this;
        var deferred = $q.defer();
        promise.then(
          // success
          function(res) {
            var result = self._handleSyncLoad(res);
            if (result) {
              deferred.resolve(result);
            } else {
              deferred.reject(result);
            }
          },
          // failure
          function(res) {
            deferred.reject(res);
          }
        );

        return deferred.promise;
      }

    };
    return DashboardState;
  }]);

'use strict';

angular.module('ark-dashboard')
  .factory('LayoutStorage', function() {

    var noopStorage = {
      setItem: function() {

      },
      getItem: function() {

      },
      removeItem: function() {

      }
    };


    function LayoutStorage(options) {

      var defaults = {
        storage: noopStorage,
        storageHash: '',
        stringifyStorage: true
      };

      angular.extend(defaults, options);
      angular.extend(options, defaults);

      this.id = options.storageId;
      this.storage = options.storage;
      this.storageHash = options.storageHash;
      this.onStorageLoad = options.onStorageLoad;
      this.onStorageSave = options.onStorageSave;
      this.stringifyStorage = options.stringifyStorage;
      this.widgetDefinitions = options.widgetDefinitions;
      this.defaultLayouts = options.defaultLayouts;
      this.customAddDefaultLayouts = options.customAddDefaultLayouts;
      this.lockDefaultLayouts = options.lockDefaultLayouts;
      this.widgetButtons = options.widgetButtons;
      this.customWidgetDropdownMenu = options.customWidgetDropdownMenu;
      this.customWidgetDropdownActions = options.customWidgetDropdownActions;
      this.customLayoutDropdownMenu = options.customLayoutDropdownMenu;
      this.customLayoutDropdownActions = options.customLayoutDropdownActions;
      this.customSerializedProperties = options.customSerializedProperties;
      this.explicitSave = options.explicitSave;
      this.defaultWidgets = options.defaultWidgets;
      this.settingsModalOptions = options.settingsModalOptions;
      this.resizeDisabled = options.resizeDisabled;
      this.dragDisabled = options.dragDisabled;
      this.options = options;
      this.options.unsavedChangeCount = 0;

      this.layouts = [];
      this.states = {};
      this.load();
      this._ensureActiveLayout();
    }

    LayoutStorage.prototype = {

      add: function(layouts, addAfterLayout) {
        if (!angular.isArray(layouts)) {
          layouts = [layouts];
        }
        var self = this;
        angular.forEach(layouts, function(layout) {
          layout.dashboard = layout.dashboard || {};
          layout.dashboard.storage = self;
          layout.dashboard.storageId = layout.id = self._getLayoutId.call(self,layout);
          layout.dashboard.widgetDefinitions = self.widgetDefinitions;
          layout.dashboard.stringifyStorage = false;
          layout.dashboard.defaultWidgets = layout.defaultWidgets || self.defaultWidgets;
          layout.dashboard.widgetButtons = self.widgetButtons;
          layout.dashboard.explicitSave = self.explicitSave;
          layout.dashboard.settingsModalOptions = self.settingsModalOptions;
          layout.dashboard.customWidgetDropdownMenu = self.customWidgetDropdownMenu;
          layout.dashboard.customWidgetDropdownActions = self.customWidgetDropdownActions;
          if(!addAfterLayout){
            self.layouts.push(layout);
          } else {
            self.layouts.splice(self.layouts.indexOf(addAfterLayout) + 1, 0, layout);
          }
        });
      },

      remove: function(layout) {
        var index = this.layouts.indexOf(layout);
        if (index >= 0) {
          this.layouts.splice(index, 1);
          delete this.states[layout.id];

          // check for active
          if (layout.active && this.layouts.length) {
            var nextActive = index > 0 ? index - 1 : 0;
            this.layouts[nextActive].active = true;
          }
        }
      },

      save: function() {

        var state = {
          layouts: this._serializeLayouts(),
          states: this.states,
          storageHash: this.storageHash
        };

        if (angular.isFunction(this.onStorageSave)) {
          this.onStorageSave(this.id, state);
        }

        if (this.stringifyStorage) {
          state = JSON.stringify(state);
        }

        this.storage.setItem(this.id, state);
        this.options.unsavedChangeCount = 0;
      },

      load: function() {

        var serialized = this.storage.getItem(this.id);

        this.clear();

        if (serialized) {
          // check for promise
          if (angular.isObject(serialized) && angular.isFunction(serialized.then)) {
            this._handleAsyncLoad(serialized);
          } else {
            this._handleSyncLoad(serialized);
          }
        } else {
          this._addDefaultLayouts();
        }
      },

      clear: function() {
        this.layouts = [];
        this.states = {};
      },

      setItem: function(id, value) {
        this.states[id] = value;
        this.save();
      },

      getItem: function(id) {
        return this.states[id];
      },

      removeItem: function(id) {
        delete this.states[id];
        this.save();
      },

      getActiveLayout: function() {
        var len = this.layouts.length;
        for (var i = 0; i < len; i++) {
          var layout = this.layouts[i];
          if (layout.active) {
            return layout;
          }
        }
        return false;
      },

      _addDefaultLayouts: function() {
        if (this.customAddDefaultLayouts) {
          this.customAddDefaultLayouts();
          return;
        }
        var self = this;
        var defaults = this.lockDefaultLayouts ? { locked: true } : {};
        angular.forEach(this.defaultLayouts, function(layout) {
          self.add(angular.extend(_.clone(defaults), layout));
        });
      },

      _serializeLayouts: function() {
        var result = [];
        var self = this;
        angular.forEach(this.layouts, function(l) {
          result.push(angular.extend({
            title: l.title,
            id: l.id,
            guid: l.guid,
            active: l.active,
            locked: l.locked,
            type: l.type,
            cols: l.cols,
            rows: l.rows,
            defaultWidgets: l.dashboard.defaultWidgets
          }, _.pick(l, self.customSerializedProperties)));
        });
        return result;
      },

      _handleSyncLoad: function(serialized) {

        var deserialized;

        if (this.stringifyStorage) {
          try {

            deserialized = JSON.parse(serialized);

          } catch (e) {
            this._addDefaultLayouts();
            return;
          }
        } else {

          deserialized = serialized;

        }

        if (this.storageHash !== deserialized.storageHash) {
          this._addDefaultLayouts();
          return;
        }
        this.states = deserialized.states;
        this.add(deserialized.layouts);

        if (angular.isFunction(this.onStorageLoad)) {
          this.onStorageLoad(this.id, deserialized);
        }
      },

      _handleAsyncLoad: function(promise) {
        var self = this;
        promise.then(
          angular.bind(self, this._handleSyncLoad),
          angular.bind(self, this._addDefaultLayouts)
        );
      },

      _ensureActiveLayout: function() {
        for (var i = 0; i < this.layouts.length; i++) {
          var layout = this.layouts[i];
          if (layout.active) {
            return;
          }
        }
        if (this.layouts[0]) {
          this.layouts[0].active = true;
        }
      },

      _getLayoutId: function(layout) {
        if (layout.id) {
          return layout.id;
        }
        var max = 0;
        for (var i = 0; i < this.layouts.length; i++) {
          var id = this.layouts[i].id;
          max = Math.max(max, id * 1);
        }
        return max + 1;
      }

    };
    return LayoutStorage;
  });

'use strict';

angular.module('ark-dashboard')
  .factory('MenusModel', ['$translate', '$rootScope', function ($translate, $rootScope) {
    return function (defaultMenus, defaultActions, customMenus, customActions) {
      var menus = {};

      // init menus with defaults
      _.each(defaultMenus, function (defaultMenu, type) {
        menus[type] = {
          list: angular.copy(defaultMenu || []),
          actions: angular.copy(defaultActions[type] || {})
        };
      });

      // apply custom menus
      _.each(customMenus, function (customMenu, type) {
        var defaultMenu = menus[type];
        if (defaultMenu) {
          if (customMenu.replaceDefaults) {
            defaultMenu.list = customMenu.menu || [];
          }
          else {
            defaultMenu.list = _.union(defaultMenu.list, customMenu.menu || []);
          }

          if (customActions) {
            // combining default and custom actions together
            // custom action with the same name as default, overrides the default action
            angular.extend(defaultMenu.actions, customActions[type] || {});
          }
        }
        else {
          var customActionsFinal = (customActions && customActions[type]) || {};
          if (customMenu.addDefaultActionsFrom !== undefined) {
            customActionsFinal = angular.extend({}, defaultActions[customMenu.addDefaultActionsFrom], customActionsFinal);
          }
          menus[type] = {
            list: customMenu.menu || [],
            actions: customActionsFinal
          };
        }
      });

      var translateFunction = function () {
        _.each(menus, function (menu) {
          _.each(menu.list, function (item) {
            if (item.menuTitleKey) {
              item.menuLocalizedTitle = $translate.instant(item.menuTitleKey);
            }
          });
        });
      };
      var listener = $rootScope.$on('$translateChangeSuccess', translateFunction);
      translateFunction();

      menus.destroy = function () {
        listener();
      };

      return menus;
    };
  }]);

'use strict';

angular.module('ark-dashboard')
  .factory('WidgetDataModel', function () {
    function WidgetDataModel() {
    }

    WidgetDataModel.prototype = {
      setup: function (widget, scope) {
        this.dataAttrName = widget.dataAttrName;
        this.dataModelOptions = widget.dataModelOptions;
        this.widgetScope = scope;
      },

      updateScope: function (data) {
        if(this.dataAttrName) {
          this.widgetScope[this.dataAttrName] = data;
        }
      },

      init: function () {
        // to be overridden by subclasses
      },

      destroy: function () {
        // to be overridden by subclasses
      }
    };

    return WidgetDataModel;
  });

'use strict';

angular.module('ark-dashboard')
  .factory('WidgetDefCollection', function () {
    function WidgetDefCollection(widgetDefs) {
      this.push.apply(this, widgetDefs);

      // build (name -> widget definition) map for widget lookup by name
      var map = {};
      _.each(widgetDefs, function (widgetDef) {
        map[widgetDef.name] = widgetDef;
      });
      this.map = map;
    }

    WidgetDefCollection.prototype = Object.create(Array.prototype);

    WidgetDefCollection.prototype.getByName = function (name) {
      return this.map[name];
    };

    return WidgetDefCollection;
  });
'use strict';

angular.module('ark-dashboard')
  .factory('WidgetModel', ['$log', function ($log) {
    // constructor for widget model instances
    function WidgetModel(Class, overrides) {
      var defaults = {
        title: 'Widget',
        name: Class.name,
        hideTitle: Class.hideTitle,
        attrs: Class.attrs,
        dataAttrName: Class.dataAttrName,
        dataModelType: Class.dataModelType,
        dataModelArgs: Class.dataModelArgs, // used in data model constructor, not serialized
        //AW Need deep copy of options to support widget options editing
        dataModelOptions: Class.dataModelOptions,
        settingsModalOptions: Class.settingsModalOptions,
        onSettingsClose: Class.onSettingsClose,
        onSettingsDismiss: Class.onSettingsDismiss,
        style: Class.style || {},
        size: Class.size || {},
        enableVerticalResize: !!Class.enableVerticalResize
      };

      overrides = overrides || {};
      angular.extend(this, angular.copy(defaults), overrides);
      this.containerStyle = { width: '33%' }; // default width
      this.contentStyle = {};
      this.updateContainerStyle(this.style);

      if (Class.contentStyle) {
        this.contentStyle = Class.contentStyle;
      }

      if (Class.templateUrl) {
        this.templateUrl = Class.templateUrl;
      } else if (Class.template) {
        this.template = Class.template;
      } else {
        this.directive = Class.directive || Class.name;
      }

      if (this.size && _.has(this.size, 'height')) {
        this.setHeight(this.size.height);
      }

      if (this.style && _.has(this.style, 'width')) { //TODO deprecate style attribute
        this.setWidth(this.style.width);
      }

      if (this.size && _.has(this.size, 'width')) {
        this.setWidth(this.size.width);
      }
    }

    WidgetModel.prototype = {
      // sets the width (and widthUnits)
      setWidth: function (width, units) {
        width = width.toString();
        units = units || width.replace(/^[-\.\d]+/, '') || '%';

        this.widthUnits = units;
        width = parseFloat(width);

        if (width < 0 || isNaN(width)) {
          $log.warn('malhar-angular-dashboard: setWidth was called when width was ' + width);
          return false;
        }

        if (units === '%') {
          width = Math.min(100, width);
          width = Math.max(0, width);
        }

        this.containerStyle.width = width + '' + units;

        this.updateSize(this.containerStyle);

        return true;
      },

      setHeight: function (height) {
        this.contentStyle.height = height;
        this.updateSize(this.contentStyle);
      },

      setStyle: function (style) {
        this.style = style;
        this.updateContainerStyle(style);
      },

      updateSize: function (size) {
        angular.extend(this.size, size);
      },

      updateContainerStyle: function (style) {
        angular.extend(this.containerStyle, style);
      }
    };

    return WidgetModel;
  }]);
'use strict';

angular.module('ark-dashboard')
  .controller('DashboardWidgetCtrl', ['$scope', function($scope) {

      $scope.status = {
        isopen: false
      };

      // saves whatever is in the title input as the new title
      $scope.saveTitleEdit = function(widget) {
        widget.editingTitle = false;
        $scope.$emit('widgetChanged', widget);
      };
    }
  ]);

'use strict';

angular.module('ark-dashboard')
  .controller('renameModalCtrl', ['$scope', '$modalInstance', 'title', 'type', function ($scope, $modalInstance, title, type) {

    // set up result object
    $scope.title = title || {};
    if(type === 'Dashboard') {
      $scope.type = 'Dashboard';
    } else if (type  === 'Widget') {
      $scope.type = 'Widget';
    }

    $scope.ok = function () {
      $modalInstance.close($scope.title);
    };

    $scope.cancel = function () {
      $modalInstance.dismiss();
    };
  }]);

'use strict';

angular.module('ark-dashboard')
  .controller('SaveChangesModalCtrl', ['$scope', '$modalInstance', 'layout', function ($scope, $modalInstance, layout) {
    
    // add layout to scope
    $scope.layout = layout;

    $scope.ok = function () {
      $modalInstance.close();
    };

    $scope.cancel = function () {
      $modalInstance.dismiss();
    };
  }]);
(function(angular) {

  'use strict';

  angular.module('gridster', [])

  .constant('gridsterConfig', {
    columns: 6, // number of columns in the grid
    pushing: true, // whether to push other items out of the way
    floating: true, // whether to automatically float items up so they stack
    swapping: false, // whether or not to have items switch places instead of push down if they are the same size
    width: 'auto', // width of the grid. "auto" will expand the grid to its parent container
    colWidth: 'auto', // width of grid columns. "auto" will divide the width of the grid evenly among the columns
    rowHeight: 'match', // height of grid rows. 'match' will make it the same as the column width, a numeric value will be interpreted as pixels, '/2' is half the column width, '*5' is five times the column width, etc.
    margins: [10, 10], // margins in between grid items
    outerMargin: true,
    isMobile: false, // toggle mobile view
    mobileBreakPoint: 600, // width threshold to toggle mobile mode
    mobileModeEnabled: true, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint
    minColumns: 1, // minimum amount of columns the grid can scale down to
    minRows: 1, // minimum amount of rows to show if the grid is empty
    maxRows: 100, // maximum amount of rows in the grid
    defaultSizeX: 2, // default width of an item in columns
    defaultSizeY: 1, // default height of an item in rows
    minSizeX: 1, // minimum column width of an item
    maxSizeX: null, // maximum column width of an item
    minSizeY: 1, // minumum row height of an item
    maxSizeY: null, // maximum row height of an item
    saveGridItemCalculatedHeightInMobile: false, // grid item height in mobile display. true- to use the calculated height by sizeY given
    resizable: { // options to pass to resizable handler
      enabled: true,
      handles: ['s', 'e', 'n', 'w', 'se', 'ne', 'sw', 'nw']
    },
    draggable: { // options to pass to draggable handler
      enabled: true,
      scrollSensitivity: 20, // Distance in pixels from the edge of the viewport after which the viewport should scroll, relative to pointer
      scrollSpeed: 15 // Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance
    }
  })

  .controller('GridsterCtrl', ['gridsterConfig', '$timeout',
    function(gridsterConfig, $timeout) {

      var gridster = this;

      /**
       * Create options from gridsterConfig constant
       */
      angular.extend(this, gridsterConfig);

      this.resizable = angular.extend({}, gridsterConfig.resizable || {});
      this.draggable = angular.extend({}, gridsterConfig.draggable || {});

      var flag = false;
      this.layoutChanged = function() {
        if (flag) {
          return;
        }
        flag = true;
        $timeout(function() {
          flag = false;
          if (gridster.loaded) {
            gridster.floatItemsUp();
          }
          gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY + 1 : 0,
                                gridster.movingItem ? gridster.movingItem.curRow : 0);
        });
      };

      /**
       * A positional array of the items in the grid
       */
      this.grid = [];

      /**
       * Clean up after yourself
       */
      this.destroy = function() {
        if (this.grid) {
          this.grid.length = 0;
          this.grid = null;
        }
      };

      /**
       * Overrides default options
       *
       * @param {object} options The options to override
       */
      this.setOptions = function(options) {
        if (!options) {
          return;
        }

        options = angular.extend({}, options);

        // all this to avoid using jQuery...
        if (options.draggable) {
          angular.extend(this.draggable, options.draggable);
          delete(options.draggable);
        }
        if (options.resizable) {
          angular.extend(this.resizable, options.resizable);
          delete(options.resizable);
        }

        angular.extend(this, options);

        if (!this.margins || this.margins.length !== 2) {
          this.margins = [0, 0];
        } else {
          for (var x = 0, l = this.margins.length; x < l; ++x) {
            this.margins[x] = parseInt(this.margins[x], 10);
            if (isNaN(this.margins[x])) {
              this.margins[x] = 0;
            }
          }
        }
      };

      /**
       * Check if item can occupy a specified position in the grid
       *
       * @param {object} item The item in question
       * @param {number} row The row index
       * @param {number} column The column index
       * @returns {boolean} True if if item fits
       */
      this.canItemOccupy = function(item, row, column) {
        return row > -1 && column > -1 && item.sizeX + column <= this.columns && item.sizeY + row <= this.maxRows;
      };

      /**
       * Set the item in the first suitable position
       *
       * @param {object} item The item to insert
       */
      this.autoSetItemPosition = function(item) {
        // walk through each row and column looking for a place it will fit
        for (var rowIndex = 0; rowIndex < this.maxRows; ++rowIndex) {
          for (var colIndex = 0; colIndex < this.columns; ++colIndex) {
            // only insert if position is not already taken and it can fit
            var items = this.getItems(rowIndex, colIndex, item.sizeX, item.sizeY, item);
            if (items.length === 0 && this.canItemOccupy(item, rowIndex, colIndex)) {
              this.putItem(item, rowIndex, colIndex);
              return;
            }
          }
        }
        throw new Error('Unable to place item!');
      };

      /**
       * Gets items at a specific coordinate
       *
       * @param {number} row
       * @param {number} column
       * @param {number} sizeX
       * @param {number} sizeY
       * @param {array} excludeItems An array of items to exclude from selection
       * @returns {array} Items that match the criteria
       */
      this.getItems = function(row, column, sizeX, sizeY, excludeItems) {
        var items = [];
        if (!sizeX || !sizeY) {
          sizeX = sizeY = 1;
        }
        if (excludeItems && !(excludeItems instanceof Array)) {
          excludeItems = [excludeItems];
        }
        for (var h = 0; h < sizeY; ++h) {
          for (var w = 0; w < sizeX; ++w) {
            var item = this.getItem(row + h, column + w, excludeItems);
            if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) {
              items.push(item);
            }
          }
        }
        return items;
      };

      this.getAllItems = function() {
        var items = [];
        if(this.grid) {
          this.grid.forEach(function (line) {
            line.forEach(function (item) {
              items.push(item);
            });
          });
        }
        return items;
      };

      this.reset = function() {
        this.grid = [];
      };

      this.getBoundingBox = function(items) {

        if (items.length === 0) {
          return null;
        }
        if (items.length === 1) {
          return {
            row: items[0].row,
            col: items[0].col,
            sizeY: items[0].sizeY,
            sizeX: items[0].sizeX
          };
        }

        var maxRow = 0;
        var maxCol = 0;
        var minRow = 9999;
        var minCol = 9999;

        for (var i = 0, l = items.length; i < l; ++i) {
          var item = items[i];
          minRow = Math.min(item.row, minRow);
          minCol = Math.min(item.col, minCol);
          maxRow = Math.max(item.row + item.sizeY, maxRow);
          maxCol = Math.max(item.col + item.sizeX, maxCol);
        }

        return {
          row: minRow,
          col: minCol,
          sizeY: maxRow - minRow,
          sizeX: maxCol - minCol
        };
      };


      /**
       * Removes an item from the grid
       *
       * @param {object} item
       */
      this.removeItem = function(item) {
        for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
          var columns = this.grid[rowIndex];
          if (!columns) {
            continue;
          }
          var index = columns.indexOf(item);
          if (index !== -1) {
            delete columns[index];
            break;
          }
        }
        this.layoutChanged();
      };

      /**
       * Returns the item at a specified coordinate
       *
       * @param {number} row
       * @param {number} column
       * @param {array} excludeitems Items to exclude from selection
       * @returns {object} The matched item or null
       */
      this.getItem = function(row, column, excludeItems) {
        if (excludeItems && !(excludeItems instanceof Array)) {
          excludeItems = [excludeItems];
        }
        var sizeY = 1;
        while (row > -1) {
          var sizeX = 1,
            col = column;
          while (col > -1) {
            var items = this.grid[row];
            if (items) {
              var item = items[col];
              if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && item.sizeX >= sizeX && item.sizeY >= sizeY) {
                return item;
              }
            }
            ++sizeX;
            --col;
          }
          --row;
          ++sizeY;
        }
        return null;
      };

      /**
       * Insert an array of items into the grid
       *
       * @param {array} items An array of items to insert
       */
      this.putItems = function(items) {
        for (var i = 0, l = items.length; i < l; ++i) {
          this.putItem(items[i]);
        }
      };

      /**
       * Insert a single item into the grid
       *
       * @param {object} item The item to insert
       * @param {number} row (Optional) Specifies the items row index
       * @param {number} column (Optional) Specifies the items column index
       * @param {array} ignoreItems
       */
      this.putItem = function(item, row, column, ignoreItems) {
        if (typeof row === 'undefined' || row === null) {
          row = item.row;
          column = item.col;
          if (typeof row === 'undefined' || row === null) {
            this.autoSetItemPosition(item);
            return;
          }
        }
        if (!this.canItemOccupy(item, row, column)) {
          column = Math.min(this.columns - item.sizeX, Math.max(0, column));
          row = Math.min(this.maxRows - item.sizeY, Math.max(0, row));
        }

        if (item.oldRow !== null && typeof item.oldRow !== 'undefined') {
          var samePosition = item.oldRow === row && item.oldColumn === column;
          var inGrid = this.grid[row] && this.grid[row][column] === item;
          if (samePosition && inGrid) {
            item.row = row;
            item.col = column;
            return;
          } else {
            // remove from old position
            var oldRow = this.grid[item.oldRow];
            if (oldRow && oldRow[item.oldColumn] === item) {
              delete oldRow[item.oldColumn];
            }
          }
        }

        item.oldRow = item.row = row;
        item.oldColumn = item.col = column;

        this.moveOverlappingItems(item, ignoreItems);

        if (!this.grid[row]) {
          this.grid[row] = [];
        }
        this.grid[row][column] = item;

        if (this.movingItem === item) {
          this.floatItemUp(item);
        }
        this.layoutChanged();
      };

      /**
       * Trade row and column if item1 with item2
       *
       * @param {object} item1
       * @param {object} item2
       */
      this.swapItems = function(item1, item2) {
        this.grid[item1.row][item1.col] = item2;
        this.grid[item2.row][item2.col] = item1;

        var item1Row = item1.row;
        var item1Col = item1.col;
        item1.row = item2.row;
        item1.col = item2.col;
        item2.row = item1Row;
        item2.col = item1Col;
      };

      /**
       * Prevents items from being overlapped
       *
       * @param {object} item The item that should remain
       * @param {array} ignoreItems
       */
      this.moveOverlappingItems = function(item, ignoreItems) {
        if (ignoreItems) {
          if (ignoreItems.indexOf(item) === -1) {
            ignoreItems = ignoreItems.slice(0);
            ignoreItems.push(item);
          }
        } else {
          ignoreItems = [item];
        }
        var overlappingItems = this.getItems(
          item.row,
          item.col,
          item.sizeX,
          item.sizeY,
          ignoreItems
        );
        this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems);
      };

      /**
       * Moves an array of items to a specified row
       *
       * @param {array} items The items to move
       * @param {number} newRow The target row
       * @param {array} ignoreItems
       */
      this.moveItemsDown = function(items, newRow, ignoreItems) {
        if (!items || items.length === 0) {
          return;
        }
        items.sort(function(a, b) {
          return a.row - b.row;
        });
        ignoreItems = ignoreItems ? ignoreItems.slice(0) : [];
        var topRows = {},
          item, i, l;
        // calculate the top rows in each column
        for (i = 0, l = items.length; i < l; ++i) {
          item = items[i];
          var topRow = topRows[item.col];
          if (typeof topRow === 'undefined' || item.row < topRow) {
            topRows[item.col] = item.row;
          }
        }
        // move each item down from the top row in its column to the row
        for (i = 0, l = items.length; i < l; ++i) {
          item = items[i];
          var rowsToMove = newRow - topRows[item.col];
          this.moveItemDown(item, item.row + rowsToMove, ignoreItems);
          ignoreItems.push(item);
        }
      };

      this.moveItemDown = function(item, newRow, ignoreItems) {
        if (item.row >= newRow) {
          return;
        }
        while (item.row < newRow) {
          ++item.row;
          this.moveOverlappingItems(item, ignoreItems);
        }
        this.putItem(item, item.row, item.col, ignoreItems);
      };

      /**
       * Moves all items up as much as possible
       */
      this.floatItemsUp = function() {
        if (this.floating === false) {
          return;
        }
        if (this.grid) {
          for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
            var columns = this.grid[rowIndex];
            if (!columns) {
              continue;
            }
            for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
              var item = columns[colIndex];
              if (item) {
                this.floatItemUp(item);
              }
            }
          }
        }
      };

      /**
       * Float an item up to the most suitable row
       *
       * @param {object} item The item to move
       */
      this.floatItemUp = function(item) {
        if (this.floating === false) {
          return;
        }
        var colIndex = item.col,
          sizeY = item.sizeY,
          sizeX = item.sizeX,
          bestRow = null,
          bestColumn = null,
          rowIndex = item.row - 1;

        while (rowIndex > -1) {
          var items = this.getItems(rowIndex, colIndex, sizeX, sizeY, item);
          if (items.length !== 0) {
            break;
          }
          bestRow = rowIndex;
          bestColumn = colIndex;
          --rowIndex;
        }
        if (bestRow !== null) {
          this.putItem(item, bestRow, bestColumn);
        }
      };

      /**
       * Update gridsters height
       *
       * @param {number} plus (Optional) Additional height to add
       * @param {number} currentRow (Optional) row number to be used for adding height
       */
      this.updateHeight = function(plus, currentRow) {
        if (this.grid) {
          var maxHeight = this.minRows;
          plus = plus || 0;

          for (var rowIndex = this.grid.length; rowIndex >= 0; --rowIndex) {
            var columns = this.grid[rowIndex];
            if (!columns) {
              continue;
            }
            for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
              if (columns[colIndex]) {
                maxHeight = Math.max(maxHeight, rowIndex + plus + columns[colIndex].sizeY);
              }
            }
          }
          if (currentRow) {
            maxHeight = Math.max(maxHeight, currentRow + plus);
          }
          this.gridHeight = this.maxRows - maxHeight > 0 ? Math.min(this.maxRows, maxHeight) : Math.max(this.maxRows, maxHeight);
        }
      };

      /**
       * Returns the number of rows that will fit in given amount of pixels
       *
       * @param {number} pixels
       * @param {boolean} ceilOrFloor (Optional) Determines rounding method
       */
      this.pixelsToRows = function(pixels, ceilOrFloor) {
        if (ceilOrFloor === true) {
          return Math.ceil(pixels / this.curRowHeight);
        } else if (ceilOrFloor === false) {
          return Math.floor(pixels / this.curRowHeight);
        }

        return Math.round(pixels / this.curRowHeight);
      };

      /**
       * Returns the number of columns that will fit in a given amount of pixels
       *
       * @param {number} pixels
       * @param {boolean} ceilOrFloor (Optional) Determines rounding method
       * @returns {number} The number of columns
       */
      this.pixelsToColumns = function(pixels, ceilOrFloor) {
        if (ceilOrFloor === true) {
          return Math.ceil(pixels / this.curColWidth);
        } else if (ceilOrFloor === false) {
          return Math.floor(pixels / this.curColWidth);
        }

        return Math.round(pixels / this.curColWidth);
      };

      // unified input handling
      // adopted from a msdn blogs sample
      this.unifiedInput = function(target, startEvent, moveEvent, endEvent) {
        var lastXYById = {};

        //  Opera doesn't have Object.keys so we use this wrapper
        var numberOfKeys = function(theObject) {
          if (Object.keys) {
            return Object.keys(theObject).length;
          }

          var n = 0,
            key;
          for (key in theObject) {
            ++n;
          }

          return n;
        };

        //  this calculates the delta needed to convert pageX/Y to offsetX/Y because offsetX/Y don't exist in the TouchEvent object or in Firefox's MouseEvent object
        var computeDocumentToElementDelta = function(theElement) {
          var elementLeft = 0;
          var elementTop = 0;
          var oldIEUserAgent = navigator.userAgent.match(/\bMSIE\b/);

          for (var offsetElement = theElement; offsetElement !== null; offsetElement = offsetElement.offsetParent) {
            //  the following is a major hack for versions of IE less than 8 to avoid an apparent problem on the IEBlog with double-counting the offsets
            //  this may not be a general solution to IE7's problem with offsetLeft/offsetParent
            if (oldIEUserAgent &&
              (!document.documentMode || document.documentMode < 8) &&
              offsetElement.currentStyle.position === 'relative' && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position === 'relative' && offsetElement.offsetLeft === offsetElement.offsetParent.offsetLeft) {
              // add only the top
              elementTop += offsetElement.offsetTop;
            } else {
              elementLeft += offsetElement.offsetLeft;
              elementTop += offsetElement.offsetTop;
            }
          }

          return {
            x: elementLeft,
            y: elementTop
          };
        };

        var useSetReleaseCapture = false;

        //  cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart)
        var documentToTargetDelta = computeDocumentToElementDelta(target);

        //  common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events
        var doEvent = function(theEvtObj) {

          if (theEvtObj.type === 'mousemove' && numberOfKeys(lastXYById) === 0) {
            return;
          }

          var prevent = true;

          var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj];
          for (var i = 0; i < pointerList.length; ++i) {
            var pointerObj = pointerList[i];
            var pointerId = (typeof pointerObj.identifier !== 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId !== 'undefined') ? pointerObj.pointerId : 1;

            //  use the pageX/Y coordinates to compute target-relative coordinates when we have them (in ie < 9, we need to do a little work to put them there)
            if (typeof pointerObj.pageX === 'undefined') {
              //  initialize assuming our source element is our target
              pointerObj.pageX = pointerObj.offsetX + documentToTargetDelta.x;
              pointerObj.pageY = pointerObj.offsetY + documentToTargetDelta.y;

              if (pointerObj.srcElement.offsetParent === target && document.documentMode && document.documentMode === 8 && pointerObj.type === 'mousedown') {
                //  source element is a child piece of VML, we're in IE8, and we've not called setCapture yet - add the origin of the source element
                pointerObj.pageX += pointerObj.srcElement.offsetLeft;
                pointerObj.pageY += pointerObj.srcElement.offsetTop;
              } else if (pointerObj.srcElement !== target && !document.documentMode || document.documentMode < 8) {
                //  source element isn't the target (most likely it's a child piece of VML) and we're in a version of IE before IE8 -
                //  the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents
                //  to get the document-relative coordinates (the same as pageX/Y)
                var sx = -2,
                  sy = -2; // adjust for old IE's 2-pixel border
                for (var scrollElement = pointerObj.srcElement; scrollElement !== null; scrollElement = scrollElement.parentNode) {
                  sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0;
                  sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0;
                }

                pointerObj.pageX = pointerObj.clientX + sx;
                pointerObj.pageY = pointerObj.clientY + sy;
              }
            }


            var pageX = pointerObj.pageX;
            var pageY = pointerObj.pageY;

            if (theEvtObj.type.match(/(start|down)$/i)) {
              //  clause for processing MSPointerDown, touchstart, and mousedown

              //  refresh the document-to-target delta on start in case the target has moved relative to document
              documentToTargetDelta = computeDocumentToElementDelta(target);

              //  protect against failing to get an up or end on this pointerId
              if (lastXYById[pointerId]) {
                if (endEvent) {
                  endEvent({
                    target: theEvtObj.target,
                    which: theEvtObj.which,
                    pointerId: pointerId,
                    pageX: pageX,
                    pageY: pageY
                  });
                }

                delete lastXYById[pointerId];
              }

              if (startEvent) {
                if (prevent) {
                  prevent = startEvent({
                    target: theEvtObj.target,
                    which: theEvtObj.which,
                    pointerId: pointerId,
                    pageX: pageX,
                    pageY: pageY
                  });
                }
              }

              //  init last page positions for this pointer
              lastXYById[pointerId] = {
                x: pageX,
                y: pageY
              };

              // IE pointer model
              if (target.msSetPointerCapture) {
                target.msSetPointerCapture(pointerId);
              } else if (theEvtObj.type === 'mousedown' && numberOfKeys(lastXYById) === 1) {
                if (useSetReleaseCapture) {
                  target.setCapture(true);
                } else {
                  document.addEventListener('mousemove', doEvent, false);
                  document.addEventListener('mouseup', doEvent, false);
                }
              }
            } else if (theEvtObj.type.match(/move$/i)) {
              //  clause handles mousemove, MSPointerMove, and touchmove

              if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) {
                //  only extend if the pointer is down and it's not the same as the last point

                if (moveEvent && prevent) {
                  prevent = moveEvent({
                    target: theEvtObj.target,
                    which: theEvtObj.which,
                    pointerId: pointerId,
                    pageX: pageX,
                    pageY: pageY
                  });
                }

                //  update last page positions for this pointer
                lastXYById[pointerId].x = pageX;
                lastXYById[pointerId].y = pageY;
              }
            } else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) {
              //  clause handles up/end/cancel

              if (endEvent && prevent) {
                prevent = endEvent({
                  target: theEvtObj.target,
                  which: theEvtObj.which,
                  pointerId: pointerId,
                  pageX: pageX,
                  pageY: pageY
                });
              }

              //  delete last page positions for this pointer
              delete lastXYById[pointerId];

              //  in the Microsoft pointer model, release the capture for this pointer
              //  in the mouse model, release the capture or remove document-level event handlers if there are no down points
              //  nothing is required for the iOS touch model because capture is implied on touchstart
              if (target.msReleasePointerCapture) {
                target.msReleasePointerCapture(pointerId);
              } else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) {
                if (useSetReleaseCapture) {
                  target.releaseCapture();
                } else {
                  document.removeEventListener('mousemove', doEvent, false);
                  document.removeEventListener('mouseup', doEvent, false);
                }
              }
            }
          }

          if (prevent) {
            if (theEvtObj.preventDefault) {
              theEvtObj.preventDefault();
            }

            if (theEvtObj.preventManipulation) {
              theEvtObj.preventManipulation();
            }

            if (theEvtObj.preventMouseEvent) {
              theEvtObj.preventMouseEvent();
            }
          }
        };

        // saving the settings for contentZooming and touchaction before activation
        var contentZooming, msTouchAction;

        this.enable = function() {

          if (window.navigator.msPointerEnabled) {
            //  Microsoft pointer model
            target.addEventListener('MSPointerDown', doEvent, false);
            target.addEventListener('MSPointerMove', doEvent, false);
            target.addEventListener('MSPointerUp', doEvent, false);
            target.addEventListener('MSPointerCancel', doEvent, false);

            //  css way to prevent panning in our target area
            if (typeof target.style.msContentZooming !== 'undefined') {
              contentZooming = target.style.msContentZooming;
              target.style.msContentZooming = 'none';
            }

            //  new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target
            //  without this, you cannot touch draw on the element because IE will intercept the touch events
            if (typeof target.style.msTouchAction !== 'undefined') {
              msTouchAction = target.style.msTouchAction;
              target.style.msTouchAction = 'none';
            }
          } else if (target.addEventListener) {
            //  iOS touch model
            target.addEventListener('touchstart', doEvent, false);
            target.addEventListener('touchmove', doEvent, false);
            target.addEventListener('touchend', doEvent, false);
            target.addEventListener('touchcancel', doEvent, false);

            //  mouse model
            target.addEventListener('mousedown', doEvent, false);

            //  mouse model with capture
            //  rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
            if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
              useSetReleaseCapture = true;

              target.addEventListener('mousemove', doEvent, false);
              target.addEventListener('mouseup', doEvent, false);
            }
          } else if (target.attachEvent && target.setCapture) {
            //  legacy IE mode - mouse with capture
            useSetReleaseCapture = true;
            target.attachEvent('onmousedown', function() {
              doEvent(window.event);
              window.event.returnValue = false;
              return false;
            });
            target.attachEvent('onmousemove', function() {
              doEvent(window.event);
              window.event.returnValue = false;
              return false;
            });
            target.attachEvent('onmouseup', function() {
              doEvent(window.event);
              window.event.returnValue = false;
              return false;
            });
          }
        };

        this.disable = function() {
          if (window.navigator.msPointerEnabled) {
            //  Microsoft pointer model
            target.removeEventListener('MSPointerDown', doEvent, false);
            target.removeEventListener('MSPointerMove', doEvent, false);
            target.removeEventListener('MSPointerUp', doEvent, false);
            target.removeEventListener('MSPointerCancel', doEvent, false);

            //  reset zooming to saved value
            if (contentZooming) {
              target.style.msContentZooming = contentZooming;
            }

            // reset touch action setting
            if (msTouchAction) {
              target.style.msTouchAction = msTouchAction;
            }
          } else if (target.removeEventListener) {
            //  iOS touch model
            target.removeEventListener('touchstart', doEvent, false);
            target.removeEventListener('touchmove', doEvent, false);
            target.removeEventListener('touchend', doEvent, false);
            target.removeEventListener('touchcancel', doEvent, false);

            //  mouse model
            target.removeEventListener('mousedown', doEvent, false);

            //  mouse model with capture
            //  rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
            if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
              useSetReleaseCapture = true;

              target.removeEventListener('mousemove', doEvent, false);
              target.removeEventListener('mouseup', doEvent, false);
            }
          } else if (target.detachEvent && target.setCapture) {
            //  legacy IE mode - mouse with capture
            useSetReleaseCapture = true;
            target.detachEvent('onmousedown');
            target.detachEvent('onmousemove');
            target.detachEvent('onmouseup');
          }
        };

        return this;
      };

    }
  ])

  /**
   * The gridster directive
   *
   * @param {object} $parse
   * @param {object} $timeout
   */
  .directive('gridster', ['$timeout', '$rootScope', '$window',
    function($timeout, $rootScope, $window) {
      return {
        restrict: 'EAC',
        // without transclude, some child items may lose their parent scope
        transclude: true,
        replace: true,
        template: '<div ng-class="gridsterClass()"><div ng-style="previewStyle()" class="gridster-item gridster-preview-holder"></div><div class="gridster-content" ng-transclude></div></div>',
        controller: 'GridsterCtrl',
        controllerAs: 'gridster',
        scope: {
          config: '=?gridster'
        },
        compile: function() {

          return function(scope, $elem, attrs, gridster) {
            gridster.loaded = false;

            scope.gridsterClass = function() {
              return {
                gridster: true,
                'gridster-desktop': !gridster.isMobile,
                'gridster-mobile': gridster.isMobile,
                'gridster-loaded': gridster.loaded
              };
            };

            /**
             * @returns {Object} style object for preview element
             */
            scope.previewStyle = function() {
              if (!gridster.movingItem) {
                return {
                  display: 'none'
                };
              }

              return {
                display: 'block',
                height: (gridster.movingItem.sizeY * gridster.curRowHeight - gridster.margins[0]) + 'px',
                width: (gridster.movingItem.sizeX * gridster.curColWidth - gridster.margins[1]) + 'px',
                top: (gridster.movingItem.row * gridster.curRowHeight + (gridster.outerMargin ? gridster.margins[0] : 0)) + 'px',
                left: (gridster.movingItem.col * gridster.curColWidth + (gridster.outerMargin ? gridster.margins[1] : 0)) + 'px'
              };
            };

            var updateHeight = function() {
              $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px');
            };

            var refresh = function(width) {
              // resolve "auto" & "match" values
              if (gridster.width === 'auto') {
                if (width !== undefined) {
                  gridster.curWidth  = width;
                } else {
                  gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
                }
              } else {
                gridster.curWidth = gridster.width;
              }

              if (gridster.colWidth === 'auto') {
                gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
              } else {
                gridster.curColWidth = gridster.colWidth;
              }

              gridster.curRowHeight = gridster.rowHeight;
              if (typeof gridster.rowHeight === 'string') {
                if (gridster.rowHeight === 'match') {
                  gridster.curRowHeight = Math.round(gridster.curColWidth);
                } else if (gridster.rowHeight.indexOf('*') !== -1) {
                  gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
                } else if (gridster.rowHeight.indexOf('/') !== -1) {
                  gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
                }
              }

              gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint;

              // loop through all items and reset their CSS
              for (var rowIndex = 0, l = gridster.grid.length; rowIndex < l; ++rowIndex) {
                var columns = gridster.grid[rowIndex];
                if (!columns) {
                  continue;
                }

                for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
                  if (columns[colIndex]) {
                    var item = columns[colIndex];
                    item.setElementPosition();
                    item.setElementSizeY();
                    item.setElementSizeX();
                  }
                }
              }

              updateHeight();
            };

            // update grid items on config changes
            scope.$watch('config', function () {
              gridster.setOptions(scope.config);
              refresh();
              resize();
            }, true);

            scope.$watch('config.draggable', function() {
              $rootScope.$broadcast('gridster-draggable-changed');
            }, true);

            scope.$watch('config.resizable', function() {
              $rootScope.$broadcast('gridster-resizable-changed');
            }, true);

            scope.$watch('gridster.gridHeight', updateHeight);

            scope.$watch('gridster.movingItem', function() {
              gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY + 1 : 0,
                                    gridster.movingItem ? gridster.movingItem.curRow : 0);
            });

            function getElemWidth() {
               return $elem.width();
            }

            scope.$on('gridster-resized', function() {
              if(scope.config.widgetsPositionService) {
                var widgetsPositionService = scope.config.widgetsPositionService;
                var columns = widgetsPositionService.breakpoints(gridster.curWidth);
                if(columns && scope.config.columns !== columns) {
                  scope.config.columns = columns;
                }

                $timeout(function() {
                  var items = gridster.getAllItems();
                  var needRearrange = widgetsPositionService.needRearrange(items, columns);
                  if(scope.forceRearrange || needRearrange) {
                    gridster.reset();
                    items = widgetsPositionService.itemReorder(items);
                    items.forEach(function(item) {
                      if(item.sizeX > columns) {
                        item.sizeX = columns;
                      } else if(item.sizeX < item.minSizeX) {
                        item.sizeX = item.minSizeX;
                      }
                      item.col = null;
                      item.row = null;
                      gridster.autoSetItemPosition(item);
                    });
                    scope.forceRearrange = false;
                    refresh(getElemWidth());
                  }
                });
              }
            });

            var prevWidth = 0;
            scope.forceRearrange = true;

            function resize() {
              var width = getElemWidth();

              if((!width || width === prevWidth || gridster.movingItem) && !scope.forceRearrange) {
                return;
              }

              prevWidth = width;

              if (gridster.loaded) {
                $elem.removeClass('gridster-loaded');
              }

              refresh(width);

              if (gridster.loaded) {
                $elem.addClass('gridster-loaded');
              }

              scope.$parent.$broadcast('gridster-resized');
            }

            // track element width changes any way we can
            function onResize() {
              $timeout(resize);
            }
            if (typeof $elem.resize === 'function') {
              $elem.resize(onResize);
            }
            var $win = angular.element($window);
            $win.on('resize', onResize);

            // be sure to cleanup
            scope.$on('$destroy', function() {
              gridster.destroy();
              $win.off('resize', onResize);
            });

            // allow a little time to place items before floating up
            $timeout(function() {
              scope.$watch('gridster.floating', function() {
                gridster.floatItemsUp();
              });
              gridster.loaded = true;
            }, 100);
          };
        }
      };
    }
  ])

  .controller('GridsterItemCtrl', function() {
    this.$element = null;
    this.gridster = null;
    this.row = null;
    this.col = null;
    this.sizeX = null;
    this.sizeY = null;
    this.minSizeX = 0;
    this.minSizeY = 0;
    this.maxSizeX = null;
    this.maxSizeY = null;

    this.init = function($element, gridster) {
      this.$element = $element;
      this.gridster = gridster;
      this.sizeX = gridster.defaultSizeX;
      this.sizeY = gridster.defaultSizeY;
    };

    this.destroy = function() {
      this.gridster = null;
      this.$element = null;
    };

    /**
     * Returns the items most important attributes
     */
    this.toJSON = function() {
      return {
        row: this.row,
        col: this.col,
        sizeY: this.sizeY,
        sizeX: this.sizeX
      };
    };

    this.isMoving = function() {
      return this.gridster.movingItem === this;
    };

    /**
     * Set the items position
     *
     * @param {number} row
     * @param {number} column
     */
    this.setPosition = function(row, column) {
      this.gridster.putItem(this, row, column);

      if (!this.isMoving()) {
        this.setElementPosition();
      }
    };

    /**
     * Sets a specified size property
     *
     * @param {string} key Can be either "x" or "y"
     * @param {number} value The size amount
     */
    this.setSize = function(key, value, preventMove) {
      key = key.toUpperCase();
      var camelCase = 'size' + key,
        titleCase = 'Size' + key;
      if (value === '') {
        return;
      }
      value = parseInt(value, 10);
      if (isNaN(value) || value === 0) {
        value = this.gridster['default' + titleCase];
      }
      var max = key === 'X' ? this.gridster.columns : this.gridster.maxRows;
      if (this['max' + titleCase]) {
        max = Math.min(this['max' + titleCase], max);
      }
      if (this.gridster['max' + titleCase]) {
        max = Math.min(this.gridster['max' + titleCase], max);
      }
      if (key === 'X' && this.cols) {
        max -= this.cols;
      } else if (key === 'Y' && this.rows) {
        max -= this.rows;
      }

      var min = 0;
      if (this['min' + titleCase]) {
        min = Math.max(this['min' + titleCase], min);
      }
      if (this.gridster['min' + titleCase]) {
        min = Math.max(this.gridster['min' + titleCase], min);
      }

      value = Math.max(Math.min(value, max), min);

      var changed = (this[camelCase] !== value || (this['old' + titleCase] && this['old' + titleCase] !== value));
      this['old' + titleCase] = this[camelCase] = value;

      if (!this.isMoving()) {
        this['setElement' + titleCase]();
      }
      if (!preventMove && changed) {
        this.gridster.moveOverlappingItems(this);
        this.gridster.layoutChanged();
      }

      return changed;
    };

    /**
     * Sets the items sizeY property
     *
     * @param {number} rows
     */
    this.setSizeY = function(rows, preventMove) {
      return this.setSize('Y', rows, preventMove);
    };

    /**
     * Sets the items sizeX property
     *
     * @param {number} rows
     */
    this.setSizeX = function(columns, preventMove) {
      return this.setSize('X', columns, preventMove);
    };

    /**
     * Sets an elements position on the page
     *
     * @param {number} row
     * @param {number} column
     */
    this.setElementPosition = function() {
      if (this.gridster.isMobile) {
        this.$element.css({
          marginLeft: this.gridster.margins[0] + 'px',
          marginRight: this.gridster.margins[0] + 'px',
          marginTop: this.gridster.margins[1] + 'px',
          marginBottom: this.gridster.margins[1] + 'px',
          top: '',
          left: ''
        });
      } else {
        this.$element.css({
          margin: 0,
          top: (this.row * this.gridster.curRowHeight + (this.gridster.outerMargin ? this.gridster.margins[0] : 0)) + 'px',
          left: (this.col * this.gridster.curColWidth + (this.gridster.outerMargin ? this.gridster.margins[1] : 0)) + 'px'
        });
      }
    };

    /**
     * Sets an elements height
     */
    this.setElementSizeY = function() {
      if (this.gridster.isMobile && !this.gridster.saveGridItemCalculatedHeightInMobile) {
        this.$element.css('height', '');
      } else {
        this.$element.css('height', (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]) + 'px');
      }
    };

    /**
     * Sets an elements width
     */
    this.setElementSizeX = function() {
      if (this.gridster.isMobile) {
        this.$element.css('width', '');
      } else {
        this.$element.css('width', (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]) + 'px');
      }
    };

    /**
     * Gets an element's width
     */
    this.getElementSizeX = function() {
      return (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]);
    };

    /**
     * Gets an element's height
     */
    this.getElementSizeY = function() {
      return (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]);
    };

  })

  .factory('GridsterDraggable', ['$document', '$timeout', '$window',
    function($document, $timeout, $window) {
      function GridsterDraggable($el, scope, gridster, item, itemOptions) {

        var elmX, elmY, elmW, elmH,

          mouseX = 0,
          mouseY = 0,
          lastMouseX = 0,
          lastMouseY = 0,
          mOffX = 0,
          mOffY = 0,

          minTop = 0,
          maxTop = 9999,
          minLeft = 0,
          realdocument = $document[0];

        var originalCol, originalRow;
        var inputTags = ['select', 'input', 'textarea', 'button'];

        function mouseDown(e) {
          if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) {
            return false;
          }

          // exit, if a resize handle was hit
          if (angular.element(e.target).hasClass('gridster-item-resizable-handler')) {
            return false;
          }

          // exit, if the target has it's own click event
          if (angular.element(e.target).attr('onclick') || angular.element(e.target).attr('ng-click')) {
            return false;
          }

          switch (e.which) {
          case 1:
            // left mouse button
            break;
          case 2:
          case 3:
            // right or middle mouse button
            return;
          }

          lastMouseX = e.pageX;
          lastMouseY = e.pageY;

          elmX = parseInt($el.css('left'), 10);
          elmY = parseInt($el.css('top'), 10);
          elmW = $el[0].offsetWidth;
          elmH = $el[0].offsetHeight;

          originalCol = item.col;
          originalRow = item.row;

          dragStart(e);

          return true;
        }

        function mouseMove(e) {
          if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
            return false;
          }

          var maxLeft = gridster.curWidth - 1;

          // Get the current mouse position.
          mouseX = e.pageX;
          mouseY = e.pageY;

          // Get the deltas
          var diffX = mouseX - lastMouseX + mOffX;
          var diffY = mouseY - lastMouseY + mOffY;
          mOffX = mOffY = 0;

          // Update last processed mouse positions.
          lastMouseX = mouseX;
          lastMouseY = mouseY;

          var dX = diffX,
            dY = diffY;
          if (elmX + dX < minLeft) {
            diffX = minLeft - elmX;
            mOffX = dX - diffX;
          } else if (elmX + elmW + dX > maxLeft) {
            diffX = maxLeft - elmX - elmW;
            mOffX = dX - diffX;
          }

          if (elmY + dY < minTop) {
            diffY = minTop - elmY;
            mOffY = dY - diffY;
          } else if (elmY + elmH + dY > maxTop) {
            diffY = maxTop - elmY - elmH;
            mOffY = dY - diffY;
          }
          elmX += diffX;
          elmY += diffY;

          // set new position
          $el.css({
            'top': elmY + 'px',
            'left': elmX + 'px'
          });

          drag(e);

          return true;
        }

        function mouseUp(e) {
          if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
            return false;
          }

          mOffX = mOffY = 0;

          dragStop(e);

          return true;
        }

        function dragStart(event) {
          $el.addClass('gridster-item-moving');
          gridster.movingItem = item;

          item.curRow = gridster.pixelsToRows(elmY);
          gridster.updateHeight(item.sizeY + 1, item.curRow);

          scope.$apply(function() {
            if (gridster.draggable && gridster.draggable.start) {
              gridster.draggable.start(event, $el, itemOptions);
            }
          });
        }

        function drag(event) {
          var oldRow = item.row,
            oldCol = item.col,
            hasCallback = gridster.draggable && gridster.draggable.drag,
            scrollSensitivity = gridster.draggable.scrollSensitivity,
            scrollSpeed = gridster.draggable.scrollSpeed;

          var row = gridster.pixelsToRows(elmY);
          var col = gridster.pixelsToColumns(elmX);

          var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item);
          var hasItemsInTheWay = itemsInTheWay.length !== 0;

          item.curRow = row;
          gridster.updateHeight(item.sizeY + 1, item.curRow);

          if (gridster.swapping === true && hasItemsInTheWay) {
            var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay);
            var sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY;
            var sameRow = boundingBoxItem.row === row;
            var sameCol = boundingBoxItem.col === col;
            var samePosition = sameRow && sameCol;
            var inline = sameRow || sameCol;

            if (sameSize && itemsInTheWay.length === 1) {
              if (samePosition) {
                gridster.swapItems(item, itemsInTheWay[0]);
              } else if (inline) {
                return;
              }
            } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) {
              var emptyRow = item.row <= row ? item.row : row + item.sizeY;
              var emptyCol = item.col <= col ? item.col : col + item.sizeX;
              var rowOffset = emptyRow - boundingBoxItem.row;
              var colOffset = emptyCol - boundingBoxItem.col;

              for (var i = 0, l = itemsInTheWay.length; i < l; ++i) {
                var itemInTheWay = itemsInTheWay[i];

                var itemsInFreeSpace = gridster.getItems(
                  itemInTheWay.row + rowOffset,
                  itemInTheWay.col + colOffset,
                  itemInTheWay.sizeX,
                  itemInTheWay.sizeY,
                  item
                );

                if (itemsInFreeSpace.length === 0) {
                  gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset);
                }
              }
            }
          }

          if (gridster.pushing !== false || !hasItemsInTheWay) {
            item.row = row;
            item.col = col;
          }

          if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) {
            realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed;
          } else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) {
            realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed;
          }

          if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) {
            realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed;
          } else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) {
            realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed;
          }

          if (hasCallback || oldRow !== item.row || oldCol !== item.col) {
            scope.$apply(function() {
              if (hasCallback) {
                gridster.draggable.drag(event, $el, itemOptions);
              }
            });
          }
        }

        function dragStop(event) {
          $el.removeClass('gridster-item-moving');
          var row = gridster.pixelsToRows(elmY);
          var col = gridster.pixelsToColumns(elmX);
          if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) {
            item.row = row;
            item.col = col;
          }
          gridster.movingItem = null;
          item.setPosition(item.row, item.col);

          scope.$apply(function() {
            if (gridster.draggable && gridster.draggable.stop) {
              gridster.draggable.stop(event, $el, itemOptions);
            }
          });
        }

        var enabled = false;
        var $dragHandle = null;
        var unifiedInput;

        this.enable = function() {
          var self = this;
          // disable and timeout required for some template rendering
          $timeout(function() {
            self.disable();

            if (gridster.draggable && gridster.draggable.handle) {
              $dragHandle = angular.element($el[0].querySelector(gridster.draggable.handle));
              if ($dragHandle.length === 0) {
                // fall back to element if handle not found...
                $dragHandle = $el;
              }
            } else {
              $dragHandle = $el;
            }

            unifiedInput = new gridster.unifiedInput($dragHandle[0], mouseDown, mouseMove, mouseUp);
            unifiedInput.enable();

            enabled = true;
          });
        };

        this.disable = function() {
          if (!enabled) {
            return;
          }

          unifiedInput.disable();
          unifiedInput = undefined;
          enabled = false;
        };

        this.toggle = function(enabled) {
          if (enabled) {
            this.enable();
          } else {
            this.disable();
          }
        };

        this.destroy = function() {
          this.disable();
        };
      }

      return GridsterDraggable;
    }
  ])

  .factory('GridsterResizable', [
    function() {
      function GridsterResizable($el, scope, gridster, item, itemOptions) {

        function ResizeHandle(handleClass) {

          var hClass = handleClass;

          var elmX, elmY, elmW, elmH,

            mouseX = 0,
            mouseY = 0,
            lastMouseX = 0,
            lastMouseY = 0,
            mOffX = 0,
            mOffY = 0,

            minTop = 0,
            maxTop = 9999,
            minLeft = 0;

          var getMinHeight = function() {
            var elmMinRows = item.minSizeY ? item.minSizeY : (gridster.minSizeY ? gridster.minSizeY : 1);
            return gridster.curRowHeight * elmMinRows - gridster.margins[0];
          };
          var getMinWidth = function() {
            var elmMinCols = item.minSizeX ? item.minSizeX : (gridster.minSizeX ? gridster.minSizeX : 1);
            return gridster.curColWidth * elmMinCols - gridster.margins[1];
          };
          var getMaxHeight = function() {
            var elmMaxRows = item.maxSizeY ? item.maxSizeY : (gridster.maxSizeY ? gridster.maxSizeY : 1);
            return gridster.curRowHeight * elmMaxRows - gridster.margins[0];
          };
          var getMaxWidth = function() {
            var elmMaxCols = item.maxSizeX ? item.maxSizeX : (gridster.maxSizeX ? gridster.maxSizeX : 1);
            return gridster.curColWidth * elmMaxCols - gridster.margins[1];
          };

          var originalWidth, originalHeight;
          var savedDraggable;

          function mouseDown(e) {
            switch (e.which) {
            case 1:
              // left mouse button
              break;
            case 2:
            case 3:
              // right or middle mouse button
              return;
            }

            // save the draggable setting to restore after resize
            savedDraggable = gridster.draggable.enabled;
            if (savedDraggable) {
              gridster.draggable.enabled = false;
              scope.$broadcast('gridster-draggable-changed');
            }

            // Get the current mouse position.
            lastMouseX = e.pageX;
            lastMouseY = e.pageY;

            // Record current widget dimensions
            elmX = parseInt($el.css('left'), 10);
            elmY = parseInt($el.css('top'), 10);
            elmW = $el[0].offsetWidth;
            elmH = $el[0].offsetHeight;

            originalWidth = item.sizeX;
            originalHeight = item.sizeY;

            resizeStart(e);

            return true;
          }

          function resizeStart(e) {
            $el.addClass('gridster-item-moving');
            $el.addClass('gridster-item-resizing');

            gridster.movingItem = item;

            item.setElementSizeX();
            item.setElementSizeY();
            item.setElementPosition();
            gridster.updateHeight(1);

            scope.$apply(function() {
              // callback
              if (gridster.resizable && gridster.resizable.start) {
                gridster.resizable.start(e, $el, itemOptions); // options is the item model
              }
            });
          }

          function mouseMove(e) {
            scope.$broadcast('gridster-draggable-item-resizing');
            var maxLeft = gridster.curWidth - 1;

            // Get the current mouse position.
            mouseX = e.pageX;
            mouseY = e.pageY;

            // Get the deltas
            var diffX = mouseX - lastMouseX + mOffX;
            var diffY = mouseY - lastMouseY + mOffY;
            mOffX = mOffY = 0;

            // Update last processed mouse positions.
            lastMouseX = mouseX;
            lastMouseY = mouseY;

            var dY = diffY,
              dX = diffX;

            if (hClass.indexOf('n') >= 0) {
              if (elmH - dY < getMinHeight()) {
                diffY = elmH - getMinHeight();
                mOffY = dY - diffY;
              } else if (elmH - dY > getMaxHeight()) {
                diffY = elmH - getMaxHeight();
                mOffY = dY - diffY;
              } else if (elmY + dY < minTop) {
                diffY = minTop - elmY;
                mOffY = dY - diffY;
              }
              elmY += diffY;
              elmH -= diffY;
            }
            if (hClass.indexOf('s') >= 0) {
              if (elmH + dY < getMinHeight()) {
                diffY = getMinHeight() - elmH;
                mOffY = dY - diffY;
              } else if (elmH + dY > getMaxHeight()) {
                diffY = getMaxHeight() - elmH;
                mOffY = dY - diffY;
              } else if (elmY + elmH + dY > maxTop) {
                diffY = maxTop - elmY - elmH;
                mOffY = dY - diffY;
              }
              elmH += diffY;
            }
            if (hClass.indexOf('w') >= 0) {
              if (elmW - dX < getMinWidth()) {
                diffX = elmW - getMinWidth();
                mOffX = dX - diffX;
              } else if (elmW - dX > getMaxWidth()) {
                diffX = elmW - getMaxWidth();
                mOffX = dX - diffX;
              } else if (elmX + dX < minLeft) {
                diffX = minLeft - elmX;
                mOffX = dX - diffX;
              }
              elmX += diffX;
              elmW -= diffX;
            }
            if (hClass.indexOf('e') >= 0) {
              if (elmW + dX < getMinWidth()) {
                diffX = getMinWidth() - elmW;
                mOffX = dX - diffX;
              } else if (elmW + dX > getMaxWidth()) {
                diffX = getMaxWidth() - elmW;
                mOffX = dX - diffX;
              } else if (elmX + elmW + dX > maxLeft) {
                diffX = maxLeft - elmX - elmW;
                mOffX = dX - diffX;
              }
              elmW += diffX;
            }

            // set new position
            $el.css({
              'top': elmY + 'px',
              'left': elmX + 'px',
              'width': elmW + 'px',
              'height': elmH + 'px'
            });

            resize(e);

            return true;
          }

          function mouseUp(e) {
            // restore draggable setting to its original state
            if (gridster.draggable.enabled !== savedDraggable) {
              gridster.draggable.enabled = savedDraggable;
              scope.$broadcast('gridster-draggable-changed');
            }

            mOffX = mOffY = 0;

            resizeStop(e);

            return true;
          }

          function resize(e) {
            var oldRow = item.row,
              oldCol = item.col,
              oldSizeX = item.sizeX,
              oldSizeY = item.sizeY,
              hasCallback = gridster.resizable && gridster.resizable.resize;

            var col = item.col;
            // only change column if grabbing left edge
            if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) {
              col = gridster.pixelsToColumns(elmX, false);
            }

            var row = item.row;
            // only change row if grabbing top edge
            if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) {
              row = gridster.pixelsToRows(elmY, false);
            }

            var sizeX = item.sizeX;
            // only change row if grabbing left or right edge
            if (['n', 's'].indexOf(handleClass) === -1) {
              sizeX = gridster.pixelsToColumns(elmW, true);
            }

            var sizeY = item.sizeY;
            // only change row if grabbing top or bottom edge
            if (['e', 'w'].indexOf(handleClass) === -1) {
              sizeY = gridster.pixelsToRows(elmH, true);
            }

            if (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0) {
              item.row = row;
              item.col = col;
              item.sizeX = sizeX;
              item.sizeY = sizeY;
            }
            var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY;

            scope.$apply(function() {
              scope.$broadcast('gridster-item-resizing');
            });

            if (hasCallback || isChanged) {
              scope.$apply(function() {
                if (hasCallback) {
                  gridster.resizable.resize(e, $el, itemOptions); // options is the item model
                }
              });
            }
          }

          function resizeStop(e) {
            $el.removeClass('gridster-item-moving');
            $el.removeClass('gridster-item-resizing');

            gridster.movingItem = null;

            item.setPosition(item.row, item.col);
            item.setSizeY(item.sizeY);
            item.setSizeX(item.sizeX);

            scope.$apply(function() {
              scope.$broadcast('gridster-item-resized');
            });

            scope.$apply(function() {
              if (gridster.resizable && gridster.resizable.stop) {
                gridster.resizable.stop(e, $el, itemOptions); // options is the item model
              }
            });
          }

          var $dragHandle = null;
          var unifiedInput;

          this.enable = function() {
            if (!$dragHandle) {
              $dragHandle = angular.element('<div class="gridster-item-resizable-handler handle-' + hClass + '"></div>');
              $el.append($dragHandle);
            }

            unifiedInput = new gridster.unifiedInput($dragHandle[0], mouseDown, mouseMove, mouseUp);
            unifiedInput.enable();
          };

          this.disable = function() {
            if ($dragHandle) {
              $dragHandle.remove();
              $dragHandle = null;
            }

            unifiedInput.disable();
            unifiedInput = undefined;
          };

          this.destroy = function() {
            this.disable();
          };
        }

        var handles = [];
        var handlesOpts = gridster.resizable.handles;
        if (typeof handlesOpts === 'string') {
          handlesOpts = gridster.resizable.handles.split(',');
        }
        var enabled = false;

        for (var c = 0, l = handlesOpts.length; c < l; c++) {
          handles.push(new ResizeHandle(handlesOpts[c]));
        }

        this.enable = function() {
          if (enabled) {
            return;
          }
          for (var c = 0, l = handles.length; c < l; c++) {
            handles[c].enable();
          }
          enabled = true;
        };

        this.disable = function() {
          if (!enabled) {
            return;
          }
          for (var c = 0, l = handles.length; c < l; c++) {
            handles[c].disable();
          }
          enabled = false;
        };

        var isItemResizable = function() {
          var _minSizeX = item.minSizeX ? item.minSizeX : 1;
          var _maxSizeX = item.maxSizeX ? item.maxSizeX : Infinity;
          var _minSizeY = item.minSizeY ? item.minSizeY : 1;
          var _maxSizeY = item.maxSizeY ? item.maxSizeY : Infinity;
          return _minSizeX !== _maxSizeX || _minSizeY !== _maxSizeY;
        };

        this.toggle = function(enabled) {
          if (enabled && isItemResizable()) {
            this.enable();
          } else {
            this.disable();
          }
        };

        this.destroy = function() {
          for (var c = 0, l = handles.length; c < l; c++) {
            handles[c].destroy();
          }
          handles = null;
        };
      }
      return GridsterResizable;
    }
  ])

  /**
   * GridsterItem directive
   */
  .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable',
    function($parse, GridsterDraggable, GridsterResizable) {
      return {
        restrict: 'EA',
        controller: 'GridsterItemCtrl',
        require: ['^gridster', 'gridsterItem'],
        link: function(scope, $el, attrs, controllers) {
          var optionsKey = attrs.gridsterItem,
            options;

          var gridster = controllers[0],
            item = controllers[1];

          // bind the item's position properties
          if (optionsKey) {
            var $optionsGetter = $parse(optionsKey);
            options = $optionsGetter(scope) || {};
            if (!options && $optionsGetter.assign) {
              options = {
                row: item.row,
                col: item.col,
                sizeX: item.sizeX,
                sizeY: item.sizeY,
                minSizeX: 0,
                minSizeY: 0,
                maxSizeX: null,
                maxSizeY: null
              };
              $optionsGetter.assign(scope, options);
            }
          } else {
            options = attrs;
          }

          item.init($el, gridster);
          item.orderIndex = options.orderIndex;

          $el.addClass('gridster-item');

          var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'],
            $getters = {};

          var aspectFn = function(aspect) {
            var key;
            if (typeof options[aspect] === 'string') {
              key = options[aspect];
            } else if (typeof options[aspect.toLowerCase()] === 'string') {
              key = options[aspect.toLowerCase()];
            } else if (optionsKey) {
              key = $parse(optionsKey + '.' + aspect);
            } else {
              return;
            }
            $getters[aspect] = $parse(key);

            // when the value changes externally, update the internal item object
            scope.$watch(key, function(newVal) {
              newVal = parseInt(newVal, 10);
              if (!isNaN(newVal)) {
                item[aspect] = newVal;
              }
            });

            // initial set
            var val = $getters[aspect](scope);
            if (typeof val === 'number') {
              item[aspect] = val;
            }
          };

          for (var i = 0, l = aspects.length; i < l; ++i) {
            aspectFn(aspects[i]);
          }

          scope.$broadcast('gridster-item-initialized', [item.sizeY, item.sizeX, item.getElementSizeY(), item.getElementSizeX()]);

          function positionChanged() {
            // call setPosition so the element and gridster controller are updated
            item.setPosition(item.row, item.col);

            // when internal item position changes, update externally bound values
            if ($getters.row && $getters.row.assign) {
              $getters.row.assign(scope, item.row);
            }
            if ($getters.col && $getters.col.assign) {
              $getters.col.assign(scope, item.col);
            }

            scope.$broadcast('gridster-item-moving');
          }
          scope.$watch(function() {
            return item.row + ',' + item.col;
          }, positionChanged);

          function sizeChanged() {
            var changedX = item.setSizeX(item.sizeX, true);
            if (changedX && $getters.sizeX && $getters.sizeX.assign) {
              $getters.sizeX.assign(scope, item.sizeX);
            }
            var changedY = item.setSizeY(item.sizeY, true);
            if (changedY && $getters.sizeY && $getters.sizeY.assign) {
              $getters.sizeY.assign(scope, item.sizeY);
            }

            if (changedX || changedY) {
              item.gridster.moveOverlappingItems(item);
              gridster.layoutChanged();
            }
          }
          scope.$watch(function() {
            return item.sizeY + ',' + item.sizeX + '|' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY;
          }, sizeChanged);

          var draggable = new GridsterDraggable($el, scope, gridster, item, options);
          var resizable = new GridsterResizable($el, scope, gridster, item, options);

          scope.$on('gridster-draggable-changed', function() {
            draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
          });
          scope.$on('gridster-resizable-changed', function() {
            resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
          });
          scope.$on('gridster-resized', function() {
            resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
          });
          scope.$watch(function() {
            return gridster.isMobile;
          }, function() {
            resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
            draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
          });

          function whichTransitionEvent() {
            var el = document.createElement('div');
            var transitions = {
              'transition': 'transitionend',
              'OTransition': 'oTransitionEnd',
              'MozTransition': 'transitionend',
              'WebkitTransition': 'webkitTransitionEnd'
            };
            for (var t in transitions) {
              if (el.style[t] !== undefined) {
                return transitions[t];
              }
            }
          }

          $el.on(whichTransitionEvent(), function() {
            scope.$apply(function() {
              scope.$emit('gridster-item-transition-end');
              scope.$broadcast('gridster-item-transition-end');
            });
          });

          return scope.$on('$destroy', function() {
            try {
              resizable.destroy();
              draggable.destroy();
            } catch (e) {}

            try {
              gridster.removeItem(item);
            } catch (e) {}

            try {
              item.destroy();
            } catch (e) {}
          });
        }
      };
    }
  ])

  ;

})(angular);

'use strict';

angular.module('ark-dashboard')
   .directive('dropdownMenu', function () {
    return {
      restrict: 'A',
      templateUrl: function() {
        return 'src/dashboard/components/dropdown-menu/dropdown-menu.html';
      },
      controller: ["$scope", function ($scope) {
        $scope.enabled = function (item) {
          return !item.disabled || !item.disabled($scope.layout);
        };
      }],
      scope: {
        menu: '=',
        menuActions: '=',
        layout: '=',
        widget: '=?'
      }
    };
  })
  .controller('dropdownMenuController', ['$scope', 'dropdownMenuService', function ($scope, dropdownMenuService) {
    $scope.handleMenuOption = function (action) {
      if($scope.menuActions && $scope.menuActions[action] && typeof $scope.menuActions[action] === 'function') {
        if ($scope.widget) {
          $scope.menuActions[action]($scope.layout, $scope.widget);
        } else {
          $scope.menuActions[action]($scope.layout);
        }
      }
    };
    $scope.$on('scrollingTabsEvent', function () {
      dropdownMenuService.changeActiveDropdownMenuOrientation();
    });
  }])
  .service('dropdownMenuService', [function () {
    var tabsMarginRight = 60;
    this.changeDropdownMenuOrientation = function(menu, leftOffset) {
      var width = angular.element(window).width();
      if (menu.width() + leftOffset + tabsMarginRight > width) {
        menu.addClass('dropdown-menu-pull-right')
          .removeClass('dropdown-menu-pull-left');
      } else {
        menu.removeClass('dropdown-menu-pull-right')
          .addClass('dropdown-menu-pull-left');
      }
    };
    this.changeActiveDropdownMenuOrientation = function() {
      var menu = angular.element('.open .nav-tabs-dropdown-menu');
      if (menu.length) {
        var offsetLeft = menu.prev().offset().left;
        this.changeDropdownMenuOrientation(menu, offsetLeft);
      }
    };
  }])
  .directive('selectText', ['$timeout', function ($timeout) {
    return {
      scope: {
        trigger: '@focus'
      },
      link: function(scope, element) {
        scope.$watch('trigger', function() {
          $timeout(function() {
            element.select();
          });
        });
      }
    };
  }]);

(function() {
  'use strict';

  var CONSTANTS = {
      CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL: 50, // timeout interval for repeatedly moving the tabs container
      // by one increment while the mouse is held down--decrease to
      // make mousedown continous scrolling faster
      SCROLL_OFFSET_FRACTION: 6, // each click moves the container this fraction of the fixed container--decrease
      // to make the tabs scroll farther per click
      DATA_KEY_IS_MOUSEDOWN: 'ismousedown'
    },

    /* *************************************************************
     * scrolling-tabs element directive template
     * *************************************************************/
    // plunk: http://plnkr.co/edit/YhKiIhuAPkpAyacu6tuk
    scrollingTabsTemplate = [
      '<div class="scrtabs-tab-container">',
      ' <div class="scrtabs-tab-scroll-arrow scrtabs-js-tab-scroll-arrow-left"><span class="fonticon  icon-chevron-small-left scr-arrow-left"></span></div>',
      '   <div class="scrtabs-tabs-fixed-container">',
      '     <div class="scrtabs-tabs-movable-container">',
      '       <ul class="nav nav-tabs" role="tablist">',
      '         <li ng-class="{ \'active\': tab[propActive || \'active\'], ',
      '                         \'disabled\': tab[propDisabled || \'disabled\'] }" ',
      '             data-tab="{{tab}}" data-index="{{$index}}" ng-repeat="tab in tabsArr">',
      '           <a ng-href="{{\'#\' + tab[propPaneId || \'paneId\']}}" role="tab"',
      '                data-toggle="{{tab[propDisabled || \'disabled\'] ? \'\' : \'tab\'}}" ',
      '                ng-bind-html="sanitize(tab[propTitle || \'title\']);">',
      '           </a>',
      '         </li>',
      '       </ul>',
      '     </div>',
      ' </div>',
      ' <div class="scrtabs-tab-scroll-arrow scrtabs-js-tab-scroll-arrow-right"><span class="fonticon icon-chevron-small-right scr-arrow-right"></span></div>',
      '</div>'
    ].join(''),


    /* *************************************************************
     * scrolling-tabs-wrapper element directive template
     * *************************************************************/
    // plunk: http://plnkr.co/edit/lWeQxxecKPudK7xlQxS3
    scrollingTabsWrapperTemplate = [
      '<div class="scrtabs-tab-container">',
      ' <div class="scrtabs-tab-scroll-arrow scrtabs-js-tab-scroll-arrow-left"><span class="fonticon icon-chevron-small-left scr-arrow-left"></span></div>',
      '   <div class="scrtabs-tabs-fixed-container">',
      '     <div class="scrtabs-tabs-movable-container" ng-transclude></div>',
      '   </div>',
      ' <div class="scrtabs-tab-scroll-arrow scrtabs-js-tab-scroll-arrow-right"><span class="fonticon icon-chevron-small-right scr-arrow-right"></span></div>',
      '</div>'
    ].join('');


  // smartresize from Paul Irish (debounced window resize)
  (function($, sr) {
    var debounce = function(func, threshold, execAsap) {
      var timeout;

      return function debounced() {
        var obj = this,
          args = arguments;

        function delayed() {
          if (!execAsap) {
            func.apply(obj, args);
          }
          timeout = null;
        }

        if (timeout) {
          clearTimeout(timeout);
        }
        else if (execAsap) {
          func.apply(obj, args);
        }

        timeout = setTimeout(delayed, threshold || 100);
      };
    };
    jQuery.fn[sr] = function(fn) {
      return fn ? this.bind('resize.scrtabs', debounce(fn)) : this.trigger(sr);
    };

  })(jQuery, 'smartresize');



  /* ***********************************************************************************
   * EventHandlers - Class that each instance of ScrollingTabsControl will instantiate
   * **********************************************************************************/
  function EventHandlers(scrollingTabsControl) {
    var evh = this;

    evh.stc = scrollingTabsControl;
  }

  // prototype methods
  (function(p) {
    p.handleClickOnLeftScrollArrow = function() {
      var evh = this,
        stc = evh.stc;

      stc.scrollMovement.incrementScrollLeft();
    };

    p.handleClickOnRightScrollArrow = function() {
      var evh = this,
        stc = evh.stc,
        scrollMovement = stc.scrollMovement;

      scrollMovement.incrementScrollRight(scrollMovement.getMinPos());
    };

    p.handleMousedownOnLeftScrollArrow = function() {
      var evh = this,
        stc = evh.stc;

      stc.scrollMovement.startScrollLeft();
    };

    p.handleMousedownOnRightScrollArrow = function() {
      var evh = this,
        stc = evh.stc;

      stc.scrollMovement.startScrollRight();
    };

    p.handleMouseupOnLeftScrollArrow = function() {
      var evh = this,
        stc = evh.stc;

      stc.scrollMovement.stopScrollLeft();
    };

    p.handleMouseupOnRightScrollArrow = function() {
      var evh = this,
        stc = evh.stc;

      stc.scrollMovement.stopScrollRight();
    };

    p.handleWindowResize = function() {
      var evh = this,
        stc = evh.stc,
        newWinWidth = stc.$win.width();

      if (newWinWidth === stc.winWidth) {
        return false; // false alarm
      }

      stc.winWidth = newWinWidth;
      stc.elementsHandler.refreshAllElementSizes(true); // true -> check for scroll arrows not being necessary anymore
    };

    p.addtab = function() {
      var evh = this,
        stc = evh.stc;
      stc.elementsHandler.setMovableContainerWidth();
      stc.elementsHandler.setScrollArrowVisibility();
      stc.$timeout(function() {
        stc.scrollMovement.scrollToActiveTab();
      });
    };

    p.removetab = function() {
      var evh = this,
        stc = evh.stc;
      stc.elementsHandler.setMovableContainerWidth();
      stc.elementsHandler.setScrollArrowVisibility();
      stc.$timeout(function() {
        stc.scrollMovement.scrollToActiveTab();
      });
    };

  }(EventHandlers.prototype));



  /* ***********************************************************************************
   * ElementsHandler - Class that each instance of ScrollingTabsControl will instantiate
   * **********************************************************************************/
  function ElementsHandler(scrollingTabsControl) {
    var ehd = this;

    ehd.stc = scrollingTabsControl;
  }

  // prototype methods
  (function(p) {
    p.initElements = function(isWrapperDirective) {
      var ehd = this;

      ehd.setElementReferences();

      if (isWrapperDirective) {
        ehd.moveTabContentOutsideScrollContainer();
      }

      ehd.setEventListeners();
    };

    p.moveTabContentOutsideScrollContainer = function() {
      var ehd = this,
        stc = ehd.stc,
        $tabsContainer = stc.$tabsContainer;

      $tabsContainer.find('.tab-content').appendTo($tabsContainer);
    };

    p.refreshAllElementSizes = function(isPossibleArrowVisibilityChange) {
      var ehd = this,
        stc = ehd.stc,
        smv = stc.scrollMovement,
        scrollArrowsWereVisible = stc.scrollArrowsVisible,
        minPos;

      ehd.setElementWidths();
      ehd.setScrollArrowVisibility();

      if (stc.scrollArrowsVisible) {
        ehd.setFixedContainerWidthForJustVisibleScrollArrows();
      }

      // if this was a window resize, make sure the movable container is positioned
      // correctly because, if it is far to the left and we increased the window width, it's
      // possible that the tabs will be too far left, beyond the min pos.
      if (isPossibleArrowVisibilityChange && (stc.scrollArrowsVisible || scrollArrowsWereVisible)) {
        if (stc.scrollArrowsVisible) {
          // make sure container not too far left
          minPos = smv.getMinPos();
          if (stc.movableContainerLeftPos < minPos) {
            smv.incrementScrollRight(minPos);
          } else {
            smv.scrollToActiveTab(); // true -> isOnWindowResize
          }
        } else {
          // scroll arrows went away after resize, so position movable container at 0
          stc.movableContainerLeftPos = 0;
          smv.slideMovableContainerToLeftPos();
        }
      }
    };

    p.setElementReferences = function() {
      var ehd = this,
        stc = ehd.stc,
        $tabsContainer = stc.$tabsContainer;

      stc.$fixedContainer = $tabsContainer.find('.scrtabs-tabs-fixed-container');
      stc.$movableContainer = $tabsContainer.find('.scrtabs-tabs-movable-container');
      stc.$tabsUl = $tabsContainer.find('.tabs-item');
      stc.$tabsUlActive = $tabsContainer.find('.nav-tabs');
      stc.$leftScrollArrow = $tabsContainer.find('.scrtabs-js-tab-scroll-arrow-left');
      stc.$rightScrollArrow = $tabsContainer.find('.scrtabs-js-tab-scroll-arrow-right');
      stc.$scrollArrows = stc.$leftScrollArrow.add(stc.$rightScrollArrow);

      stc.$win = jQuery(window);
    };

    p.setElementWidths = function() {
      var ehd = this,
        stc = ehd.stc;

      stc.containerWidth = stc.$tabsContainer.outerWidth(true);
      stc.winWidth = stc.$win.width();

      stc.scrollArrowsCombinedWidth = stc.$leftScrollArrow.outerWidth(true) + stc.$rightScrollArrow.outerWidth(true);

      ehd.setFixedContainerWidth();
      ehd.setMovableContainerWidth();
    };

    p.setEventListeners = function() {
      var ehd = this,
        stc = ehd.stc,
        evh = stc.eventHandlers; // eventHandlers

      stc.$leftScrollArrow.on({
        'mousedown.scrtabs': function(e) {
          evh.handleMousedownOnLeftScrollArrow.call(evh, e);
        },
        'mouseup.scrtabs': function(e) {
          evh.handleMouseupOnLeftScrollArrow.call(evh, e);
        },
        'click.scrtabs': function(e) {
          evh.handleClickOnLeftScrollArrow.call(evh, e);
        }
      });

      stc.$rightScrollArrow.on({
        'mousedown.scrtabs': function(e) {
          evh.handleMousedownOnRightScrollArrow.call(evh, e);
        },
        'mouseup.scrtabs': function(e) {
          evh.handleMouseupOnRightScrollArrow.call(evh, e);
        },
        'click.scrtabs': function(e) {
          evh.handleClickOnRightScrollArrow.call(evh, e);
        }
      });

      stc.$win.smartresize(function(e) {
        evh.handleWindowResize.call(evh, e);
      });

      stc.scope.$on('$destroy', function() {
        stc.$win.unbind('resize.scrtabs');
      });

      stc.scope.$on('activeTabChanged', function () {
        stc.$timeout(function() {
          stc.scrollMovement.scrollToActiveTab();
        });
      });


      stc.scope.$watch(
        function () {return angular.element('.tabs-item').length; },
        function (newValue, oldValue) {
          if(newValue > oldValue) {
            evh.addtab.call(evh);
          } else if (oldValue > newValue) {
            evh.removetab.call(evh);
          }
        });

    };

    p.setFixedContainerWidth = function() {
      var ehd = this,
        stc = ehd.stc;

      stc.$fixedContainer.width(stc.fixedContainerWidth = stc.$tabsContainer.outerWidth(true));
    };

    p.setFixedContainerWidthForJustHiddenScrollArrows = function() {
      var ehd = this,
        stc = ehd.stc;

      stc.$fixedContainer.width(stc.fixedContainerWidth);
    };

    p.setFixedContainerWidthForJustVisibleScrollArrows = function() {
      var ehd = this,
        stc = ehd.stc;

      stc.$fixedContainer.width(stc.fixedContainerWidth - stc.scrollArrowsCombinedWidth);
    };

    p.setMovableContainerWidth = function() {
      var ehd = this,
        stc = ehd.stc;

      stc.movableContainerWidth = stc.$tabsUlActive.outerWidth(true) - stc.$tabsUlActive.width();
      if (stc.movableContainerWidth < 0) {
        stc.movableContainerWidth = 0;
      }

      angular.element('.tabs-item').each(function __getLiWidth() {
        var $li = jQuery(this);

        stc.movableContainerWidth += $li.outerWidth(true);
      });

      stc.$movableContainer.width(stc.movableContainerWidth += 1);
    };

    p.setScrollArrowVisibility = function() {
      var ehd = this,
        stc = ehd.stc,
        shouldBeVisible = stc.movableContainerWidth > stc.fixedContainerWidth;

      if (shouldBeVisible && !stc.scrollArrowsVisible) {
        stc.$scrollArrows.show();
        stc.scrollArrowsVisible = true;
        ehd.setFixedContainerWidthForJustVisibleScrollArrows();
      } else if (!shouldBeVisible && stc.scrollArrowsVisible) {
        stc.scrollMovement.incrementScrollLeft();
        stc.$scrollArrows.hide();
        stc.scrollArrowsVisible = false;
        ehd.setFixedContainerWidthForJustHiddenScrollArrows();
      }
    };

  }(ElementsHandler.prototype));



  /* ***********************************************************************************
   * ScrollMovement - Class that each instance of ScrollingTabsControl will instantiate
   * **********************************************************************************/
  function ScrollMovement(scrollingTabsControl) {
    var smv = this;

    smv.stc = scrollingTabsControl;
  }

  // prototype methods
  (function(p) {

    p.continueScrollLeft = function() {
      var smv = this,
        stc = smv.stc;

      stc.$timeout(function() {
        if (stc.$leftScrollArrow.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN) && (stc.movableContainerLeftPos < 0)) {
          if (!smv.incrementScrollLeft()) { // scroll limit not reached, so keep scrolling
            smv.continueScrollLeft();
          }
        }
      }, CONSTANTS.CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL);
    };

    p.continueScrollRight = function(minPos) {
      var smv = this,
        stc = smv.stc;

      stc.$timeout(function() {
        if (stc.$rightScrollArrow.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN) && (stc.movableContainerLeftPos > minPos)) {
          // slide tabs LEFT -> decrease movable container's left position
          // min value is (movableContainerWidth - $tabHeader width)
          if (!smv.incrementScrollRight(minPos)) {
            smv.continueScrollRight(minPos);
          }
        }
      }, CONSTANTS.CONTINUOUS_SCROLLING_TIMEOUT_INTERVAL);
    };

    p.decrementMovableContainerLeftPos = function(minPos) {
      var smv = this,
        stc = smv.stc;

      stc.movableContainerLeftPos -= (stc.fixedContainerWidth / CONSTANTS.SCROLL_OFFSET_FRACTION);
      if (stc.movableContainerLeftPos < minPos) {
        stc.movableContainerLeftPos = minPos;
      }
    };

    p.getMinPos = function() {
      var smv = this,
        stc = smv.stc;

      return stc.scrollArrowsVisible ? (stc.fixedContainerWidth - stc.movableContainerWidth - stc.scrollArrowsCombinedWidth) : 0;
    };

    p.getMovableContainerCssLeftVal = function() {
      var smv = this,
        stc = smv.stc;

      return (stc.movableContainerLeftPos === 0) ? '0' : stc.movableContainerLeftPos + 'px';
    };

    p.incrementScrollLeft = function() {
      var smv = this,
        stc = smv.stc;

      stc.movableContainerLeftPos += (stc.fixedContainerWidth / CONSTANTS.SCROLL_OFFSET_FRACTION);
      if (stc.movableContainerLeftPos > 0) {
        stc.movableContainerLeftPos = 0;
      }

      smv.slideMovableContainerToLeftPos();

      return (stc.movableContainerLeftPos === 0); // indicates scroll limit reached
    };

    p.incrementScrollRight = function(minPos) {
      var smv = this,
        stc = smv.stc;

      smv.decrementMovableContainerLeftPos(minPos);
      smv.slideMovableContainerToLeftPos();

      return (stc.movableContainerLeftPos === minPos);
    };

    p.scrollToActiveTab = function() {
      var smv = this,
        stc = smv.stc,
        $activeTab,
        activeTabWidth,
        activeTabLeftPos,
        rightArrowLeftPos,
        leftArrowRightPos,
        overlapRight,
        overlapLeft;

      // if the active tab is not fully visible, scroll till it is
      if (!stc.scrollArrowsVisible) {
        return;
      }

      $activeTab = stc.$tabsUlActive.find('li.active-add');
      if ($activeTab.length === 0) {
        $activeTab = stc.$tabsUlActive.find('li.active');
      }

      if (!$activeTab.length) {
        return;
      }

      activeTabWidth = $activeTab.outerWidth(true);
      activeTabLeftPos = $activeTab.offset().left;

      rightArrowLeftPos = stc.$rightScrollArrow.offset().left;
      overlapRight = activeTabLeftPos + activeTabWidth - rightArrowLeftPos;

      if (overlapRight > 0) {
        stc.movableContainerLeftPos = stc.movableContainerLeftPos - overlapRight;
        smv.slideMovableContainerToLeftPos();
        return;
      }
      leftArrowRightPos = stc.$leftScrollArrow.offset().left + stc.$leftScrollArrow.width();
      overlapLeft = leftArrowRightPos - activeTabLeftPos;
      if (overlapLeft > 0) {
        stc.movableContainerLeftPos = stc.movableContainerLeftPos + overlapLeft;
        smv.slideMovableContainerToLeftPos();
      }
    };

    p.slideMovableContainerToLeftPos = function() {
      var smv = this,
        stc = smv.stc,
        leftVal;

      stc.movableContainerLeftPos = stc.movableContainerLeftPos / 1;
      leftVal = smv.getMovableContainerCssLeftVal();

      stc.$movableContainer.stop().animate({
        left: leftVal
      }, 'slow', function __slideAnimComplete() {
        var newMinPos = smv.getMinPos();

        // if we slid past the min pos--which can happen if you resize the window
        // quickly--move back into position
        if (stc.movableContainerLeftPos < newMinPos) {
          smv.decrementMovableContainerLeftPos(newMinPos);
          stc.$movableContainer.stop().animate({
            left: smv.getMovableContainerCssLeftVal()
          }, 'fast', function __slideAnimComplete() {
            stc.scope.$broadcast('scrollingTabsEvent');
          });
        }
      });
    };

    p.startScrollLeft = function() {
      var smv = this,
        stc = smv.stc;

      stc.$leftScrollArrow.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, true);
      smv.continueScrollLeft();
    };

    p.startScrollRight = function() {
      var smv = this,
        stc = smv.stc;

      stc.$rightScrollArrow.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, true);
      smv.continueScrollRight(smv.getMinPos());
    };

    p.stopScrollLeft = function() {
      var smv = this,
        stc = smv.stc;

      stc.$leftScrollArrow.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, false);
    };

    p.stopScrollRight = function() {
      var smv = this,
        stc = smv.stc;

      stc.$rightScrollArrow.data(CONSTANTS.DATA_KEY_IS_MOUSEDOWN, false);
    };

  }(ScrollMovement.prototype));



  /* **********************************************************************
   * ScrollingTabsControl - Class that each directive will instantiate
   * **********************************************************************/
  function ScrollingTabsControl(scope, $tabsContainer, $timeout) {
    var stc = this;

    stc.$tabsContainer = $tabsContainer;
    stc.$timeout = $timeout;
    stc.scope = scope;
    stc.movableContainerLeftPos = 0;
    stc.scrollArrowsVisible = true;

    stc.scrollMovement = new ScrollMovement(stc);
    stc.eventHandlers = new EventHandlers(stc);
    stc.elementsHandler = new ElementsHandler(stc);
  }

  // prototype methods
  (function(p) {
    p.initTabs = function(isWrapperDirective) {
      var stc = this,
        elementsHandler = stc.elementsHandler,
        scrollMovement = stc.scrollMovement;

      stc.$timeout(function __initTabsAfterTimeout() {
        elementsHandler.initElements(isWrapperDirective);
        elementsHandler.refreshAllElementSizes();
        elementsHandler.setScrollArrowVisibility();
        scrollMovement.scrollToActiveTab();
      }, 100);
    };


  }(ScrollingTabsControl.prototype));



  /* ********************************************************
   * scrolling-tabs Directive
   * ********************************************************/

  function scrollingTabsDirective($timeout, $sce) {

    function sanitize(html) {
      return $sce.trustAsHtml(html);
    }


    // ------------ Directive Object ---------------------------
    return {
      restrict: 'E',
      template: scrollingTabsTemplate,
      transclude: false,
      replace: true,
      scope: {
        tabs: '@',
        propPaneId: '@',
        propTitle: '@',
        propActive: '@',
        propDisabled: '@',
        localTabClick: '&tabClick'
      },
      link: function(scope, element) {
        var scrollingTabsControl = new ScrollingTabsControl(scope, element, $timeout);

        scope.tabsArr = scope.$eval(scope.tabs);
        scope.propPaneId = scope.propPaneId || 'paneId';
        scope.propTitle = scope.propTitle || 'title';
        scope.propActive = scope.propActive || 'active';
        scope.propDisabled = scope.propDisabled || 'disabled';
        scope.sanitize = sanitize;

        element.on('click.scrollingTabs', '.nav-tabs > li', function __handleClickOnTab(e) {
          var clickedTabElData = jQuery(this).data();

          scope.localTabClick({
            $event: e,
            $index: clickedTabElData.index,
            tab: clickedTabElData.tab
          });
        });

        scrollingTabsControl.initTabs(false); // false -> not the wrapper directive
      }

    };
  }

  /* ********************************************************
   * scrolling-tabs-wrapper Directive
   * ********************************************************/
  function scrollingTabsWrapperDirective($timeout) {
    // ------------ Directive Object ---------------------------
    return {
      restrict: 'A',
      template: scrollingTabsWrapperTemplate,
      transclude: true,
      replace: true,
      link: function(scope, element) {
        var scrollingTabsControl = new ScrollingTabsControl(scope, element, $timeout);

        scrollingTabsControl.initTabs(true); // true -> wrapper directive
      }
    };

  }

  scrollingTabsDirective.$inject = ['$timeout', '$sce'];
  scrollingTabsWrapperDirective.$inject = ['$timeout'];

  angular.module('ark-dashboard').directive('scrollingTabs', scrollingTabsDirective);
  angular.module('ark-dashboard').directive('scrollingTabsWrapper', scrollingTabsWrapperDirective);

}());

'use strict';

angular.module('ark-dashboard')
    .factory('dashboardLayoutsService', [function () {

        var layoutTypes = {
            'DASHBOARD': 'dashboard',
            'WALLBOARD': 'wallboard',
            'WIDGET': 'widget'
        };

        var layoutMenuOptions = {
            'RENAME': {
                'menuLocalizedTitle': 'Rename',
                'menuTitleKey': 'titles.RENAME',
                'menuIcon': 'icon-24-graph-edit',
                'menuOptionKey': 'renameLayout'
            },
            'CLOSE': {
                'menuLocalizedTitle': 'Close',
                'menuTitleKey': 'titles.CLOSE',
                'menuIcon': 'icon-close',
                'menuOptionKey': 'removeLayout'
            }
        };

        var layoutsDefaultMenus = {};
        layoutsDefaultMenus[layoutTypes.DASHBOARD] = [layoutMenuOptions.RENAME, layoutMenuOptions.CLOSE];
        layoutsDefaultMenus[layoutTypes.WALLBOARD] = [layoutMenuOptions.RENAME, layoutMenuOptions.CLOSE];
        layoutsDefaultMenus[layoutTypes.WIDGET] = [layoutMenuOptions.RENAME, layoutMenuOptions.CLOSE];

        return {
            'layoutTypes': layoutTypes,
            'layoutMenuOptions': layoutMenuOptions,
            'layoutsDefaultMenus': layoutsDefaultMenus
        };
    }]
);

'use strict';

/*
 * ark-datasource-cometd-connector
 *
 * Use this snippet: https://gist.github.com/simon-jouet/4543421
 */

angular.module('connectors')
  .factory('cometdConnector', [function() {

    var connected;
    var cometd = jQuery.cometd;

    /**
    * @ngdoc method
    * @name connect
    * @methodOf cometdConnector
    *
    * @description Method to acquire cometd connection to server
    * @param {object} options options to be passed to cometd.configure() method
    * @returns {object} promise to be resolved with cometd object on successful connect
    */
    var connect = function(options) {
      if (!connected || cometd.isDisconnected()) {
        connected = $.Deferred();
        cometd.configure(options);
        cometd.handshake(function(handshake) {
          if (connected.state() === "pending") {
            if (handshake.successful === true) {
              connected.resolve(cometd);
            }
          }
        });
      }
      return connected;
    };

    return {
      connect: connect
    };
  }]);

'use strict';

/**
 * @ngdoc overview
 * @name cark-datasource-poller-connector
 * @description Pulse Layout Service implementation
 */
angular.module('connectors')
	.factory('pollerConnector', ['$log', 'poller', function ($log, poller) {

	var PollerConnector = function() {

		/**
	     * @ngdoc method
	     * @name start
	     * @methodOf pollerConnector
	     *
	     * @description Method to get pulse user preferences from GAX backend
	     * @param {object} http resource to poll
	     * @param {int} pollingInterval frequency in ms to polll resource 
	     * @returns {object} new poller object
	     */
		this.start = function(resource, pollingInterval) { // resource is a string or an object created with $resource
			var newPoller = poller.get(resource, {delay: pollingInterval, smart: true, catchError: true});
			$log.debug('PollerInterface: start polling ' + newPoller + ' every ' + pollingInterval + ' ms');
			return newPoller;
		};

		/**
	     * @ngdoc method
	     * @name stop
	     * @methodOf pollerConnector
	     *
	     * @description Method to stop polling resource
	     * @param {object} poller to stop
	     */
		this.stop = function(_poller) {
			$log.debug('PollerInterface: stop polling ' + _poller);
			_poller.stop();
		};
		
        /**
	     * @ngdoc method
	     * @name remove
	     * @methodOf pollerConnector
	     *
	     * @description Method to remove polling resource
	     * @param {object} poller to remove
	     */
		this.remove = function(_poller) {
			$log.debug('PollerInterface: remove polling ' + _poller);
			_poller.remove();
		};

		/**
	     * @ngdoc method
	     * @name restart
	     * @methodOf pollerConnector
	     *
	     * @description Method to restart polling resource
	     * @param {object} poller to restart
	     */
		this.restart = function(_poller) {
			$log.debug('PollerInterface: restart polling ' + _poller);
			_poller.restart();
		};
	};

	return new PollerConnector();
}]);

'use strict';

angular.module('ark-dashboard')
    .factory('widgetsPositionService', [function () {
        var dashboardBreakpoints = [
            {
                width: 1024,
                columns: 2
            },
            {
                width: 1536,
                columns: 4
            },
            {
                width: 2048,
                columns: 6
            },
            {
                width: 3072,
                columns: 8
            },
            {
                width: 4096,
                columns: 12
            }
        ].sort(function (a, b) {
                return a.width - b.width;
            });

        this.needRearrange = function(gridsterItems, columnCount) {
            var doesNotFitScreen = _.some(gridsterItems, function(item) {
                return item.col + item.sizeX > columnCount;
            });
            var spaceOnTheRight = _.every(gridsterItems, function(item) {
                return item.col + item.sizeX < columnCount;
            });
            return doesNotFitScreen || spaceOnTheRight;
        };

        this.breakpoints = function(width) {
            var columns;
            for(var i = 0; i < dashboardBreakpoints.length; i++) {
                if(width < dashboardBreakpoints[i].width) {
                    columns = dashboardBreakpoints[i].columns;
                    break;
                }
            }
            return columns;
        };

        this.itemReorder = function(items) {
            if(_.every(items, function(it) {return typeof it.orderIndex !== 'undefined';})) {
                items = items.sort(function(a,b) {
                    var aInt = parseInt(a.orderIndex, 10);
                    var bInt = parseInt(b.orderIndex, 10);
                    return aInt - bInt;
                });
            }
            return items;
        };

        this.POSITION_TYPES = {
          DEFAULT: "default",
          FOOTER: "footer"
        };

        return this;
    }]
);

'use strict';

angular.module('ark-dashboard')
    .factory('widgetsService', [function () {

        var widgetTypes = {
            'DEFAULT': 'default'
        };

        var widgetMenuOptions = {
            'RENAME': {
                'menuLocalizedTitle':'Rename',
                'menuTitleKey': 'titles.RENAME',
                'menuIcon':'icon-24-graph-edit',
                'menuOptionKey':'renameWidget'
            },
            'CLONE': {
                'menuLocalizedTitle':'Clone',
                'menuTitleKey': 'titles.CLONE',
                'menuIcon':'icon-clone',
                'menuOptionKey':'cloneWidget'
            },
            'DELETE': {
                'menuLocalizedTitle':'Delete',
                'menuTitleKey': 'titles.DELETE',
                'menuIcon':'icon-trash',
                'menuOptionKey':'removeWidget'
            }
        };

        var widgetsDefaultMenus = {};
        widgetsDefaultMenus[widgetTypes.DEFAULT] = [widgetMenuOptions.RENAME, widgetMenuOptions.CLONE, widgetMenuOptions.DELETE];

        return {
            'widgetTypes': widgetTypes,
            'widgetMenuOptions': widgetMenuOptions,
            'widgetsDefaultMenus': widgetsDefaultMenus
        };
    }]
);

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

  $templateCache.put('src/dashboard/template/dashboard-layouts.html',
    "<div class=\"tabs-bar-container\">\n" +
    "  <div scrolling-tabs-wrapper class=\"tabs-container\" ng-class=\"{ 'tabs-container-with-bar-button': options.customTabsBarMenu && options.customTabsBarMenu.menuOptionKey}\">\n" +
    "    <ul ui-sortable=\"sortableOptions\" ng-model=\"layouts\" class=\"nav nav-tabs layout-tabs\">\n" +
    "\n" +
    "      <li ng-repeat=\"layout in layouts\" ng-class=\"{ active: layout.active }\" class=\"tabs-item nav-tabs-layout\">\n" +
    "        <span ng-if='layout.badgeValue'>\n" +
    "         <span class='tabs-notification-icon'>{{layout.badgeValue}}</span>\n" +
    "        </span>\n" +
    "        <a ng-click=\"options.makeLayoutActive(layout)\" ng-init=\"addTooptip()\">\n" +
    "            <span ng-if=\"layout.locked\" class=\"tabs-icon fonticon icon-secure\"></span>\n" +
    "            <span ng-if=\"layout.type === 'dashboard'\" class=\"tabs-icon fonticon icon-dashtab-dash\"></span>\n" +
    "            <span ng-if=\"layout.type === 'wallboard'\" class=\"tabs-icon fonticon icon-full-screen\"></span>\n" +
    "            <span ng-if=\"layout.type === 'widget'\" class=\"tabs-icon fonticon icon-dashtab-xwidget\"></span>\n" +
    "            <span ng-if=\"layout.type !== 'dashboard' && layout.type !== 'widget' && layout.type !== 'wallboard'\" class=\"tabs-icon fonticon {{layout.icon}}\"></span>\n" +
    "            <span class=\"tabs-title\" ng-if=\"layout.showTooltip\" tooltip-placement=\"bottom\" tooltip=\"{{layout.title}}\" tooltip-popup-delay=\"500\">{{layout.title}}</span>\n" +
    "            <span class=\"tabs-title\" ng-if=\"!layout.showTooltip\" title=\"{{layout.title}}\">{{layout.title}}</span>\n" +
    "        </a>\n" +
    "        <div class=\"nav-tabs-collapse\">\n" +
    "            <ul class=\"nav\">\n" +
    "                <li ng-if=\"layout.layoutMenu.list.length\" class=\"ark-dropdown dropdown\">\n" +
    "                    <a ng-if=\"!layout.active\" ng-click=\"options.makeLayoutActive(layout)\" class=\"ark-dropdown-toggle nav-tabs-dropdown\"><i class=\"icon-more\"></i></a>\n" +
    "                    <a ng-if=\"layout.active\" href=\"#\" role=\"button\" class=\"ark-dropdown-toggle nav-tabs-dropdown\"><i class=\"icon-more\"></i></a>\n" +
    "                    <ul ng-if=\"layout.active\" class=\"dropdown-menu nav-tabs-dropdown-menu dropdown-menu-pull-left\" role=\"menu\" dropdown-menu menu=\"layout.layoutMenu.list\" menu-actions=\"layout.layoutMenu.actions\" layout=\"layout\"></ul>\n" +
    "                </li>\n" +
    "            </ul>\n" +
    "        </div>\n" +
    "      </li>\n" +
    "    </ul>\n" +
    "  </div>\n" +
    "\n" +
    "  <div ng-if=\"options.customTabsBarMenu && options.customTabsBarMenu.menuOptionKey\" class=\"tabs-bar-button-placeholder\">\n" +
    "    <button class=\"btn\" ng-click=\"onTabsBarAction(options.customTabsBarMenu.menuOptionKey)\" title=\"{{options.customTabsBarMenu.menuLocalizedTitle}}\">\n" +
    "      <span class=\"fonticon {{options.customTabsBarMenu.menuIcon}}\"></span>\n" +
    "    </button>\n" +
    "  </div>\n" +
    "\n" +
    "  <div class=\"dashboard-layouts-container\">\n" +
    "    <div ng-repeat=\"layout in layouts | filter:isActiveNonCached\">\n" +
    "      <div ng-if=\"layout.type === 'dashboard'\"\n" +
    "        layout=\"layout\"\n" +
    "        dashboard=\"layout.dashboard\"\n" +
    "        dashboard-layouts-api=\"dashboardLayoutsApi\"\n" +
    "        template-url=\"src/dashboard/template/tab-dashboard.html\">\n" +
    "      </div>\n" +
    "      <div ng-if=\"layout.type === 'wallboard'\"\n" +
    "        layout=\"layout\"\n" +
    "        dashboard=\"layout.dashboard\"\n" +
    "        dashboard-layouts-api=\"dashboardLayoutsApi\"\n" +
    "        template-url=\"src/dashboard/template/tab-dashboard.html\">\n" +
    "      </div>\n" +
    "      <div ng-if=\"layout.type !== 'dashboard' && layout.type !== 'wallboard'\"\n" +
    "        layout=\"layout\"\n" +
    "        dashboard=\"layout.dashboard\"\n" +
    "        dashboard-layouts-api=\"dashboardLayoutsApi\"\n" +
    "        template-url=\"src/dashboard/template/tab-widget.html\">\n" +
    "      </div>\n" +
    "    </div>\n" +
    "\n" +
    "    <div ng-repeat=\"layout in layouts | filter:isCached\">\n" +
    "      <div ng-show=\"layout.active\"\n" +
    "        layout=\"layout\"\n" +
    "        dashboard=\"layout.dashboard\"\n" +
    "        dashboard-layouts-api=\"dashboardLayoutsApi\"\n" +
    "        template-url=\"src/dashboard/template/tab-widget.html\">\n" +
    "      </div>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "\n" +
    "</div>\n"
  );


  $templateCache.put('src/dashboard/template/rename-template.html',
    "<div class=\"modal-header\">\r" +
    "\n" +
    "    <span ng-click=\"cancel()\" class=\"icon-close close modal-dialog-close\"></span>\r" +
    "\n" +
    "  <h1 class=\"modal-header\">Rename {{type | htmlToPlainText}}</h1>\r" +
    "\n" +
    "</div>\r" +
    "\n" +
    "\r" +
    "\n" +
    "<div class=\"modal-body\">\r" +
    "\n" +
    "  <form name=\"form\" novalidate class=\"form-horizontal\">\r" +
    "\n" +
    "    <div class=\"form-group\" ng-class=\"{'has-error': form.$invalid, 'has-feedback': form.$invalid}\">\r" +
    "\n" +
    "      <div class=\"col-sm-10\">\r" +
    "\n" +
    "        <input type=\"text\" class=\"form-control\" name=\"title\" ng-model=\"title\" maxlength=\"255\" select-text required />\r" +
    "\n" +
    "        <span class=\"form-control-feedback\" ng-show=\"form.$invalid\">\r" +
    "\n" +
    "          <span ng-if=\"form.$error.required\"><span class=\"fonticon icon-alert-circle\"></span>This field is mandatory.</span>\r" +
    "\n" +
    "        </span>\r" +
    "\n" +
    "      </div>\r" +
    "\n" +
    "    </div>\r" +
    "\n" +
    "  </form>\r" +
    "\n" +
    "</div>\r" +
    "\n" +
    "\r" +
    "\n" +
    "<div class=\"modal-footer\">\r" +
    "\n" +
    "  <button type=\"button\" class=\"btn btn-default\" ng-click=\"cancel()\">Cancel</button>\r" +
    "\n" +
    "  <button type=\"button\" class=\"btn btn-primary\" ng-click=\"ok()\" ng-disabled=\"!form.$valid\">Rename</button>\r" +
    "\n" +
    "</div>"
  );


  $templateCache.put('src/dashboard/template/save-changes-modal.html',
    "<div class=\"modal-header\">\n" +
    "\t<button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\" ng-click=\"cancel()\">&times;</button>\n" +
    "  <h3>Unsaved Changes to \"{{layout.title | htmlToPlainText}}\"</h3>\n" +
    "</div>\n" +
    "\n" +
    "<div class=\"modal-body\">\n" +
    "  <p>You have {{layout.dashboard.unsavedChangeCount}} unsaved changes on this dashboard. Would you like to save them?</p>\n" +
    "</div>\n" +
    "\n" +
    "<div class=\"modal-footer\">\n" +
    "  <button type=\"button\" class=\"btn btn-default\" ng-click=\"cancel()\">Don't Save</button>\n" +
    "  <button type=\"button\" class=\"btn btn-primary\" ng-click=\"ok()\">Save</button>\n" +
    "</div>"
  );


  $templateCache.put('src/dashboard/template/tab-dashboard.html',
    "<div ng-class=\"{'dashboard-with-footer': hasFooterWidget()}\">\n" +
    "  <div ng-if=\"overlayOpts\" ng-style=\"overlayOpts.style\" class=\"dashboard-overlay\">\n" +
    "      <div>\n" +
    "        <span class=\"icon-alert-triangle\"></span>\n" +
    "        <label>{{overlayOpts.text}}</label>\n" +
    "      </div>\n" +
    "  </div>\n" +
    "  <div data-jquery-scrollbar class=\"dashboard-area no-select scrollbar-macosx\">\n" +
    "    <!--  gridster layout START -->\n" +
    "    <div gridster=\"gridsterOpts\" class=\"dashboard-widget-area\">\n" +
    "      <ul>\n" +
    "        <li gridster-item=\"widget\" ng-repeat=\"widget in getGridsterWidgets()\" ng-style=\"widget.containerStyle\" class=\"widget-container\">\n" +
    "          <div class=\"widget\" ng-style=\"widget.contentStyle\" ng-include=\"'src/dashboard/template/widget-default-content.html'\"></div>\n" +
    "        </li>\n" +
    "        <li ng-if=\"widgets.length === 0 && layout.defaultDashboardWidget\" gridster-item=\"layout.defaultDashboardWidget\" ng-style=\"layout.defaultDashboardWidget.containerStyle\" class=\"widget-container\">\n" +
    "          <div class=\"widget\">\n" +
    "            <div class=\"widget-controls\">\n" +
    "              <div class=\"widget-drag-button\">\n" +
    "                <span class=\"fonticon icon-grab widget-anchor\"></span>\n" +
    "              </div>\n" +
    "            </div>\n" +
    "            <div class=\"widget-title\">\n" +
    "              <div ng-if=\"!options.hideWidgetName\" class=\"widget-name-text\" title=\"{{layout.defaultDashboardWidget.title}}\">{{layout.defaultDashboardWidget.title}}</div>\n" +
    "            </div>\n" +
    "            <div class=\"widget-content\">\n" +
    "              <widget-compiler options='layout.defaultDashboardWidget'></widget-compiler>\n" +
    "            </div>\n" +
    "          </div>\n" +
    "        </li>\n" +
    "      </ul>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "</div>\n" +
    "<div ng-repeat=\"widget in getFooterWidgets()\" class=\"dashboard-footer-area widget-container\">\n" +
    "  <div class=\"widget\" ng-style=\"widget.contentStyle\" ng-include=\"'src/dashboard/template/widget-default-content.html'\"></div>\n" +
    "</div>\n"
  );


  $templateCache.put('src/dashboard/template/tab-widget.html',
    "<div class=\"dashboard-area no-select\" ng-repeat=\"widget in widgets\">\r" +
    "\n" +
    "\t<!-- START: Widget Container -->\r" +
    "\n" +
    "\t<div widget=\"widget\" class=\"widget\" ng-style=\"widget.contentStyle\">\r" +
    "\n" +
    "\t\t<widget-compiler options='widget' />\r" +
    "\n" +
    "\t</div>\r" +
    "\n" +
    "\t<!-- END: Widget Container -->\r" +
    "\n" +
    "</div>\r" +
    "\n"
  );


  $templateCache.put('src/dashboard/template/widget-default-content.html',
    "<div class=\"widget-controls\">\n" +
    "  <div class=\"widget-indicator\" ng-if=\"widget.connectionIndicator.widgetRemoved || widget.connectionIndicator.fatalError\"><span class=\"icon-alert-circle\"></span></div>\n" +
    "  <div class=\"widget-indicator\" ng-if=\"widget.connectionIndicator.supported && !widget.connectionIndicator.connected\"><span class=\"icon-alert-circle\"></span></div>\n" +
    "  <div class=\"widget-drag-button\">\n" +
    "    <span class=\"fonticon icon-grab widget-anchor\"></span>\n" +
    "  </div>\n" +
    "  <div class=\"widget-menu-collapse\" ng-hide=\"(widget.isErrorViewActive && !widget.connectionIndicator.fatalError && !widget.connectionIndicator.softError) || widget.connectionIndicator.widgetRemoved\">\n" +
    "    <ul class=\"nav\">\n" +
    "      <li class=\"ark-dropdown dropdown\">\n" +
    "        <a role=\"button\" class=\"ark-dropdown-toggle nav-tabs-dropdown\"><i class=\"icon-more\"></i></a>\n" +
    "        <ul class=\"dropdown-menu nav-tabs-dropdown-menu dropdown-menu-pull-left widget-control-dropdown\" role=\"menu\" dropdown-menu menu=\"getWidgetMenu(widget.directive).list\" menu-actions=\"getWidgetMenu(widget.directive).actions\" layout=\"layout\" widget=\"widget\"></ul>\n" +
    "      </li>\n" +
    "    </ul>\n" +
    "  </div>\n" +
    "\n" +
    "  <div class=\"widget-errorView-indicator\" ng-if=\"widget.errorViewAlertIndicator && !(widget.connectionIndicator.supported && !widget.connectionIndicator.connected) && !widget.connectionIndicator.fatalError\">\n" +
    "    <div ng-show=\"widget.errorViewAlertIndicator && !widget.isErrorViewActive\" ng-click=\"widget.errorViewIndicatorClicked()\">\n" +
    "      <span class=\"icon-alert-triangle\"></span>\n" +
    "    </div>\n" +
    "\n" +
    "    <div ng-show=\"widget.isErrorViewActive && widget.errorViewAlertIndicator\">\n" +
    "      <span class=\"icon-alert-triangle\" ng-class=\"{'widget-errorView-active': widget.isErrorViewActive && !widget.connectionIndicator.fatalError }\"></span>\n" +
    "    </div>\n" +
    "\n" +
    "    <div ng-show=\"widget.isErrorViewActive && !widget.connectionIndicator.fatalError && !widget.connectionIndicator.softError\" ng-click=\"widget.errorViewIndicatorClicked()\">\n" +
    "      <span class=\"icon-close\"></span>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "</div>\n" +
    "<div class=\"widget-title\" ng-if=\"!widget.connectionIndicator.supported || widget.connectionIndicator.connected\" ng-class=\"{'widget-errorView-active': widget.isErrorViewActive && !widget.connectionIndicator.fatalError && !widget.connectionIndicator.softError, 'widget-removed-alert': widget.connectionIndicator.widgetRemoved || widget.connectionIndicator.fatalError}\">\n" +
    "  <div class=\"widget-name-text\" ng-if=\"!options.hideWidgetName\" ng-class=\"{'widget-hidden-title': widget.hideTitle}\" title=\"{{widget.title}}\">\n" +
    "    {{widget.title}}\n" +
    "  </div>\n" +
    "</div>\n" +
    "<div class=\"widget-title-alert\" ng-if=\"widget.connectionIndicator.supported && !widget.connectionIndicator.connected\">\n" +
    "  <div class=\"widget-name-text\" ng-if=\"!options.hideWidgetName\" ng-class=\"{'widget-hidden-title': widget.hideTitle}\" title=\"{{widget.title}}\">\n" +
    "    {{widget.title}}\n" +
    "  </div>\n" +
    "</div>\n" +
    "<div class=\"widget-content\" ng-if=\"widget.connectionIndicator.supported && !widget.connectionIndicator.connected\">\n" +
    "  <div class=\"widget-error-message\" ng-if=\"widget.connectionIndicator.supported && !widget.connectionIndicator.connected\">{{\"messages.CONNECTION_FAILED\" | translate}}</div>\n" +
    "</div>\n" +
    "<div class=\"widget-content\" ng-class=\"{'widget-hidden': widget.connectionIndicator.supported && !widget.connectionIndicator.connected}\">\n" +
    "  <widget-compiler options='widget'></widget-compiler>\n" +
    "</div>\n"
  );


  $templateCache.put('src/dashboard/components/dropdown-menu/dropdown-menu.html',
    "<li title=\"{{item.menuLocalizedTitle}}\" ng-repeat=\"item in menu | filter:enabled\" role=\"presentation\" ng-controller=\"dropdownMenuController\">\r" +
    "\n" +
    "  <a role=\"menuitem\" tabindex=\"-1\" ng-click=\"handleMenuOption(item.menuOptionKey)\">\r" +
    "\n" +
    "    <i class=\"icon {{item.menuIcon}}\"></i> {{item.menuLocalizedTitle}}\r" +
    "\n" +
    "  </a>\r" +
    "\n" +
    "</li>\r" +
    "\n"
  );

}]);
