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; }