New AngularJS directives

sgDropdownContentToggle, sgUserTypeahead, sgSubscribe
pull/91/head
Francis Lachapelle 2014-11-11 10:18:45 -05:00
parent 55f0d5a959
commit 4016fdd430
2 changed files with 308 additions and 6 deletions

View File

@ -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:
<a dropdown-toggle="#dropdown-content">My Dropdown Content</a>
<div id="dropdown-content" class="sg-dropdown-content">
<div>
<h1>Hello</h1>
<p>World!</p>
</div>
</div>
*/
.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:
<div sg-subscribe="contact" sg-subscribe-on-select="subscribeToFolder"></div>
*/
.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('<span class="joyride-nub left"></span>');
}
};
}])
/*
* 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:
<div sg-subscribe="contact" sg-subscribe-on-select="subscribeToFolder"></div>
*/
.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;
});
}
};
}]);
})();

View File

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