diff --git a/UI/WebServerResources/js/Common/ui-desktop.js b/UI/WebServerResources/js/Common/ui-desktop.js
index c287c9a0c..7bc6a4fe6 100644
--- a/UI/WebServerResources/js/Common/ui-desktop.js
+++ b/UI/WebServerResources/js/Common/ui-desktop.js
@@ -120,12 +120,268 @@
.directive('sgEscape', function() {
var ESCAPE_KEY = 27;
return function (scope, elem, attrs) {
- elem.bind('keydown', function (event) {
- if (event.keyCode === ESCAPE_KEY) {
- scope.$apply(attrs.sgEscape);
- }
- });
+ elem.bind('keydown', function (event) {
+ if (event.keyCode === ESCAPE_KEY) {
+ scope.$apply(attrs.sgEscape);
+ }
+ });
};
- });
+ })
+
+/*
+ * sgDropdownContentToggle - Provides dropdown content functionality
+ * @restrict class or attribute
+ * @see https://github.com/pineconellc/angular-foundation/blob/master/src/dropdownToggle/dropdownToggle.js
+ * @example:
+
+ My Dropdown Content
+
+ */
+ .directive('sgDropdownContentToggle', ['$document', '$window', '$location', '$position', function ($document, $window, $location, $position) {
+ var openElement = null,
+ closeMenu = angular.noop;
+ return {
+ restrict: 'CA', // class and attribute
+ scope: {
+ dropdownToggle: '@sgDropdownContentToggle'
+ },
+ link: function(scope, element, attrs, controller) {
+ var dropdown = angular.element($document[0].querySelector(scope.dropdownToggle));
+
+ scope.$watch('$location.path', function() {
+ closeMenu();
+ });
+ element.bind('click', function(event) {
+ var elementWasOpen = (element === openElement);
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (!!openElement) {
+ closeMenu();
+ }
+
+ if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
+ dropdown.css('display', 'block');
+
+ var offset = $position.offset(element),
+ dropdownParentOffset = $position.offset(angular.element(dropdown[0].offsetParent)),
+ dropdownWidth = dropdown.prop('offsetWidth'),
+ dropdownHeight = dropdown.prop('offsetHeight'),
+ dropdownCss = {},
+ left = Math.round(offset.left - dropdownParentOffset.left),
+ rightThreshold = $window.innerWidth - dropdownWidth - 8,
+ nub = angular.element(dropdown.children()[0]),
+ nubCss = {};
+
+ if (left > rightThreshold) {
+ // There's more place on the left side of the element
+ left = rightThreshold;
+ dropdown.removeClass('left').addClass('right');
+ nub.removeClass('left').addClass('right');
+ }
+
+ dropdownCss.position = null;
+ dropdownCss['max-width'] = null;
+ // Place a third of the dropdown above the element
+ dropdownCss.top = Math.round(offset.top + offset.height / 2 - dropdownHeight / 3),
+ dropdownCss.left = Math.round(offset.left + offset.width + 10);
+
+ if (dropdownCss.top + dropdownHeight > $window.innerHeight) {
+ // Position dropdown at the very top of the window
+ dropdownCss.top = $window.innerHeight - dropdownHeight - 5;
+ if (dropdownHeight > $window.innerHeight) {
+ // Resize height of dropdown to fit window
+ dropdownCss.height = ($window.innerHeight - 10) + 'px';
+ }
+ }
+
+ // Place nub beside the element
+ nubCss.top = Math.round(offset.top - dropdownCss.top + offset.height / 2 - nub.prop('offsetHeight') / 2) + 'px';
+
+ // Apply CSS
+ dropdownCss.top += 'px';
+ dropdownCss.left += 'px';
+ dropdown.css(dropdownCss);
+ nub.css(nubCss);
+
+ openElement = element;
+ closeMenu = function (event) {
+ if (event) {
+ // We ignore clicks that occur inside the dropdown content element, unless it's a button
+ var target = angular.element(event.target),
+ ignoreClick = false;
+ while (target[0]) {
+ if (target[0].tagName == 'BUTTON') break;
+ if (target[0] == dropdown[0]) {
+ ignoreClick = true;
+ break;
+ }
+ target = target.parent();
+ }
+ if (ignoreClick) return;
+ }
+
+ $document.unbind('click', closeMenu);
+ dropdown.css('display', 'none');
+ closeMenu = angular.noop;
+ openElement = null;
+ };
+ $document.bind('click', closeMenu);
+ }
+ });
+
+ if (dropdown) {
+ dropdown.css('display', 'none');
+ }
+ }
+ };
+ }])
+
+/*
+ * sgSubscribe - Common subscription widget
+ * @restrict class or attribute
+ * @param {String} sgSubscribe - the folder type
+ * @param {Function} sgSubscribeOnSelect - the function to call when subscribing to a folder
+ * @example:
+
+
+ */
+ .directive('sgSubscribe', [function() {
+ return {
+ restrict: 'CA',
+ scope: {
+ folderType: '@sgSubscribe',
+ onFolderSelect: '=sgSubscribeOnSelect'
+ },
+ templateUrl: 'userFoldersTemplate', // UI/Templates/Contacts/UIxContactsUserFolders.wox
+ controller: ['$scope', function($scope) {
+ $scope.selectUser = function(i) {
+ // Fetch folders of specific type for selected user
+ $scope.users[i].$folders($scope.folderType).then(function() {
+ $scope.selectedUser = $scope.users[i];
+ });
+ };
+ $scope.selectFolder = function(folder) {
+ console.debug("select folder " + folder.displayName);
+ $scope.onFolderSelect(folder);
+ };
+ }],
+ link: function(scope, element, attrs, controller) {
+ // NOTE: We could also make these modifications in the wox template
+ element.addClass('joyride-tip-guide');
+ angular.element(element.children()[0]).addClass('joyride-content-wrapper');
+ element.prepend('');
+ }
+ };
+ }])
+
+/*
+ * sgUserTypeahead - Typeahead of users, used internally by sgSubscribe
+ * @restrict attribute
+ * @param {String} sgModel - the folder type
+ * @param {Function} sgSubscribeOnSelect - the function to call when subscribing to a folder
+ * @see https://github.com/pineconellc/angular-foundation/blob/master/src/typeahead/typeahead.js
+ * @example:
+
+
+ */
+ .directive('sgUserTypeahead', ['$parse', '$q', '$timeout', '$position', 'sgUser', function($parse, $q, $timeout, $position, User) {
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ link: function(originalScope, element, attrs, controller) {
+
+ var hasFocus,
+ scope,
+ resetMatches,
+ getMatchesAsync,
+ // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
+ timeoutPromise,
+ // Minimal number of characters that needs to be entered before typeahead kicks-in
+ minSearch = originalScope.$eval(attrs.sgSubscribeMinLength) || 3,
+ // Minimal wait time after last character typed before typehead kicks-in
+ waitTime = originalScope.$eval(attrs.sgSubscribeWaitMs) || 500,
+ // Binding to a variable that indicates if matches are being retrieved asynchronously
+ isLoadingSetter = $parse(attrs.sgSubscribeLoading).assign || angular.noop;
+
+ // Create a child scope for the typeahead directive so we are not polluting original scope
+ // with typeahead-specific data (users, query, etc.)
+ scope = originalScope.$new();
+ originalScope.$on('$destroy', function(){
+ scope.$destroy();
+ });
+
+ resetMatches = function() {
+ originalScope.users = [];
+ originalScope.selectedUser = undefined;
+ scope.activeIdx = -1;
+ };
+
+ getMatchesAsync = function(inputValue) {
+ isLoadingSetter(originalScope, true);
+ $q.when(User.$filter(inputValue)).then(function(matches) {
+ // It might happen that several async queries were in progress if a user were typing fast
+ // but we are interested only in responses that correspond to the current view value
+ if (inputValue === controller.$viewValue && hasFocus) {
+ if (matches.length > 0) {
+ scope.activeIdx = 0;
+ originalScope.users = matches;
+ originalScope.query = inputValue; // for the hightlighter
+ }
+ else {
+ resetMatches();
+ }
+ isLoadingSetter(originalScope, false);
+ }
+ }, function(){
+ resetMatches();
+ isLoadingSetter(originalScope, false);
+ });
+ };
+
+ resetMatches();
+
+ // We need to propagate user's query so we can higlight matches
+ originalScope.query = undefined;
+
+ // Plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
+ // $parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
+ controller.$parsers.unshift(function (inputValue) {
+ if (inputValue && inputValue.length >= minSearch) {
+ if (waitTime > 0) {
+ if (timeoutPromise) {
+ $timeout.cancel(timeoutPromise); // cancel previous timeout
+ }
+ timeoutPromise = $timeout(function() {
+ getMatchesAsync(inputValue);
+ }, waitTime);
+ }
+ else {
+ getMatchesAsync(inputValue);
+ }
+ }
+ else {
+ isLoadingSetter(originalScope, false);
+ resetMatches();
+ }
+ return inputValue;
+ });
+
+ element.bind('blur', function (evt) {
+ hasFocus = false;
+ });
+
+ element.bind('focus', function (evt) {
+ hasFocus = true;
+ });
+ }
+ };
+ }]);
})();
diff --git a/UI/WebServerResources/scss/ContactsUI.scss b/UI/WebServerResources/scss/ContactsUI.scss
index 7c765dcad..cff1af08e 100644
--- a/UI/WebServerResources/scss/ContactsUI.scss
+++ b/UI/WebServerResources/scss/ContactsUI.scss
@@ -587,6 +587,52 @@ $column-gutter: 0;
}
}
+.sg-dropdown-content {
+ background-color: #fff;
+ height: 300px;
+ &.joyride-tip-guide {
+ .joyride-nub {
+ &.left {
+ border-color: white !important;
+ border-top-color: transparent !important;
+ border-left-color: transparent !important;
+ border-bottom-color: transparent !important;
+ }
+ }
+ .joyride-content-wrapper {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ ul {
+ list-style-type: none;
+ &.subitems {
+ margin-left: 0;
+ }
+ }
+ li {
+ &.title {
+ background-color: $secondary-color;
+ padding: $f-dropdown-list-padding;
+ text-transform: uppercase;
+ &:hover {
+ background-color: $secondary-color;
+ }
+ }
+ &.item {
+ margin: 0 5px;
+ @include radius($input-border-radius);
+ .disabled {
+ color: #ccc;
+ }
+ }
+ @include dropdown-style();
+ }
+ }
+ }
+}
+
+
+
address {
font-style: normal;
}