diff --git a/Gruntfile.js b/Gruntfile.js index 1a8abdaa2..264ab80e3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -11,7 +11,8 @@ module.exports = function(grunt) { grunt.initConfig({ karma: { options: { - configFile: 'karma.conf.js' + configFile: 'karma.conf.js', + colors: grunt.option('color') }, watch: { // Does not work under Windows? diff --git a/dist/select.js b/dist/select.js index 830bbf838..dffdf2ebc 100644 --- a/dist/select.js +++ b/dist/select.js @@ -114,7 +114,8 @@ angular.module('ui.select', []) ctrl.open = false; ctrl.disabled = undefined; // Initialized inside uiSelect directive link function ctrl.resetSearchInput = undefined; // Initialized inside uiSelect directive link function - ctrl.refreshDelay = undefined; // Initialized inside choices directive link function + ctrl.refreshDelay = undefined; // Initialized inside uiSelectChoices directive link function + ctrl.tagging = {isActivated: false, fct: undefined}; var _searchInput = $element.querySelectorAll('input.ui-select-search'); if (_searchInput.length !== 1) { @@ -174,21 +175,24 @@ angular.module('ui.select', []) ctrl.refresh = function(refreshAttr) { if (refreshAttr !== undefined) { - // Throttle / debounce - // + // Debounce // See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L155 // FYI AngularStrap typeahead does not have debouncing: https://github.com/mgcrea/angular-strap/blob/v2.0.0-rc.4/src/typeahead/typeahead.js#L177 if (_refreshDelayPromise) { $timeout.cancel(_refreshDelayPromise); } _refreshDelayPromise = $timeout(function() { - $scope.$apply(refreshAttr); + $scope.$eval(refreshAttr); }, ctrl.refreshDelay); } }; // When the user clicks on an item inside the dropdown ctrl.select = function(item) { + if(ctrl.tagging.isActivated && !item && ctrl.search.length > 0) { + // create new item on the fly + item = ctrl.tagging.fct !== undefined ? ctrl.tagging.fct(ctrl.search) : ctrl.search; + } ctrl.selected = item; ctrl.close(); // Using a watch instead of $scope.ngModel.$setViewValue(item) @@ -217,7 +221,7 @@ angular.module('ui.select', []) if (ctrl.activeIndex < ctrl.items.length - 1) { ctrl.activeIndex++; } break; case Key.Up: - if (ctrl.activeIndex > 0) { ctrl.activeIndex--; } + if (ctrl.activeIndex > 0 || (ctrl.search.length === 0 && ctrl.tagging.isActivated)) { ctrl.activeIndex--; } break; case Key.Tab: case Key.Enter: @@ -233,11 +237,12 @@ angular.module('ui.select', []) } // Bind to keyboard shortcuts - // Cannot specify a namespace: not supported by jqLite _searchInput.on('keydown', function(e) { // Keyboard shortcuts are all about the items, // does not make sense (and will crash) if ctrl.items is empty - if (ctrl.items.length > 0) { + // unless we are in tagging mode, in that case we juste need to + // have a search term + if ((ctrl.items.length > 0 && !ctrl.tagging.isActivated) || (ctrl.search.length > 0 && ctrl.tagging.isActivated)) { var key = e.which; $scope.$apply(function() { @@ -282,8 +287,8 @@ angular.module('ui.select', []) }]) .directive('uiSelect', - ['$document', 'uiSelectConfig', - function($document, uiSelectConfig) { + ['$document', 'uiSelectConfig', 'uiSelectMinErr', + function($document, uiSelectConfig, uiSelectMinErr) { return { restrict: 'EA', @@ -314,6 +319,19 @@ angular.module('ui.select', []) $select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true; }); + attrs.$observe('tagging', function() { + if(attrs.tagging !== undefined) + { + // $eval() is needed otherwise we get a string instead of a function or a boolean + var taggingEval = scope.$eval(attrs.tagging); + $select.tagging = {isActivated: true, fct: taggingEval !== true ? taggingEval : undefined}; + } + else + { + $select.tagging = {isActivated: false, fct: undefined}; + } + }); + scope.$watch('$select.selected', function(newValue, oldValue) { if (ngModel.$viewValue !== newValue) { ngModel.$setViewValue(newValue); @@ -324,8 +342,24 @@ angular.module('ui.select', []) $select.selected = ngModel.$viewValue; }; - // See Click everywhere but here event http://stackoverflow.com/questions/12931369 - $document.on('mousedown', function(e) { + function ensureHighlightVisible() { + var container = element.querySelectorAll('.ui-select-choices-content'); + var rows = container.querySelectorAll('.ui-select-choices-row'); + + var highlighted = rows[$select.activeIndex]; + if(highlighted) { + var posY = highlighted.offsetTop + highlighted.clientHeight - container[0].scrollTop; + var height = container[0].offsetHeight; + + if (posY > height) { + container[0].scrollTop += posY - height; + } else if (posY < highlighted.clientHeight) { + container[0].scrollTop -= highlighted.clientHeight - posY; + } + } + } + + function onDocumentClick(e) { var contains = false; if (window.jQuery) { @@ -340,10 +374,13 @@ angular.module('ui.select', []) $select.close(); scope.$digest(); } - }); + } + + // See Click everywhere but here event http://stackoverflow.com/questions/12931369 + $document.on('click', onDocumentClick); scope.$on('$destroy', function() { - $document.off('mousedown'); + $document.off('click', onDocumentClick); }); // Move transcluded elements to their correct position in main template @@ -371,7 +408,7 @@ angular.module('ui.select', []) }; }]) -.directive('choices', +.directive('uiSelectChoices', ['uiSelectConfig', 'RepeatParser', 'uiSelectMinErr', function(uiSelectConfig, RepeatParser, uiSelectMinErr) { @@ -402,7 +439,7 @@ angular.module('ui.select', []) $select.parseRepeatAttr(attrs.repeat); scope.$watch('$select.search', function() { - $select.activeIndex = 0; + $select.activeIndex = $select.tagging.isActivated ? -1 : 0; $select.refresh(attrs.refresh); }); @@ -416,7 +453,7 @@ angular.module('ui.select', []) }; }]) -.directive('match', ['uiSelectConfig', function(uiSelectConfig) { +.directive('uiSelectMatch', ['uiSelectConfig', function(uiSelectConfig) { return { restrict: 'EA', require: '^uiSelect', @@ -453,10 +490,10 @@ angular.module('ui.select', []) angular.module('ui.select').run(['$templateCache', function ($templateCache) { $templateCache.put('bootstrap/choices.tpl.html', '
'); - $templateCache.put('bootstrap/match.tpl.html', ' '); + $templateCache.put('bootstrap/match.tpl.html', ' '); $templateCache.put('bootstrap/select.tpl.html', '