Skip to content
This repository was archived by the owner on Oct 2, 2019. It is now read-only.

Tagging support #63

Closed
wants to merge 10 commits into from
3 changes: 2 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
73 changes: 55 additions & 18 deletions dist/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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() {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -371,7 +408,7 @@ angular.module('ui.select', [])
};
}])

.directive('choices',
.directive('uiSelectChoices',
['uiSelectConfig', 'RepeatParser', 'uiSelectMinErr',
function(uiSelectConfig, RepeatParser, uiSelectMinErr) {

Expand Down Expand Up @@ -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);
});

Expand All @@ -416,7 +453,7 @@ angular.module('ui.select', [])
};
}])

.directive('match', ['uiSelectConfig', function(uiSelectConfig) {
.directive('uiSelectMatch', ['uiSelectConfig', function(uiSelectConfig) {
return {
restrict: 'EA',
require: '^uiSelect',
Expand Down Expand Up @@ -453,10 +490,10 @@ angular.module('ui.select', [])

angular.module('ui.select').run(['$templateCache', function ($templateCache) {
$templateCache.put('bootstrap/choices.tpl.html', '<ul class="ui-select-choices ui-select-choices-content dropdown-menu" role="menu" aria-labelledby="dLabel" ng-show="$select.items.length> 0"> <li class="ui-select-choices-row" ng-class="{active: $select.activeIndex===$index}"> <a href="javascript:void(0)" ng-transclude></a> </li> </ul> ');
$templateCache.put('bootstrap/match.tpl.html', '<button class="btn btn-default form-control ui-select-match" ng-hide="$select.open" ng-disabled="$select.disabled" ng-click="$select.activate()"> <span ng-hide="$select.selected !==undefined" class="text-muted">{{$select.placeholder}}</span> <span ng-show="$select.selected !==undefined" ng-transclude></span> <span class="caret"></span> </button> ');
$templateCache.put('bootstrap/match.tpl.html', '<button type="button" class="btn btn-default form-control ui-select-match" ng-hide="$select.open" ng-disabled="$select.disabled" ng-click="$select.activate()"> <span ng-hide="$select.selected !==undefined" class="text-muted">{{$select.placeholder}}</span> <span ng-show="$select.selected !==undefined" ng-transclude></span> <span class="caret"></span> </button> ');
$templateCache.put('bootstrap/select.tpl.html', '<div class="ui-select-bootstrap dropdown" ng-class="{open: $select.open}"> <div class="ui-select-match"></div> <input type="text" autocomplete="off" tabindex="" class="form-control ui-select-search" placeholder="{{$select.placeholder}}" ng-model="$select.search" ng-show="$select.open"> <div class="ui-select-choices"></div> </div> ');
$templateCache.put('select2/choices.tpl.html', '<ul class="ui-select-choices ui-select-choices-content select2-results"> <li class="ui-select-choices-row" ng-class="{\'select2-highlighted\': $select.activeIndex===$index}"> <div class="select2-result-label" ng-transclude></div> </li> </ul> ');
$templateCache.put('select2/match.tpl.html', '<a class="select2-choice ui-select-match" ng-class="{\'select2-default\': $select.selected === undefined}" ng-click="$select.activate()"> <span ng-hide="$select.selected !==undefined" class="select2-chosen">{{$select.placeholder}}</span> <span ng-show="$select.selected !==undefined" class="select2-chosen" ng-transclude></span> <span class="select2-arrow"><b></b></span> </a> ');
$templateCache.put('select2/match.tpl.html', '<a class="select2-choice ui-select-match" ng-class="{\'select2-default\': $select.selected===undefined}" ng-click="$select.activate()"> <span ng-hide="$select.selected !==undefined" class="select2-chosen">{{$select.placeholder}}</span> <span ng-show="$select.selected !==undefined" class="select2-chosen" ng-transclude></span> <span class="select2-arrow"><b></b></span> </a> ');
$templateCache.put('select2/select.tpl.html', '<div class="select2 select2-container" ng-class="{\'select2-container-active select2-dropdown-open\': $select.open, \'select2-container-disabled\': $select.disabled}"> <div class="ui-select-match"></div> <div class="select2-drop select2-with-searchbox select2-drop-active" ng-class="{\'select2-display-none\': !$select.open}"> <div class="select2-search"> <input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" class="ui-select-search select2-input" ng-model="$select.search"> </div> <div class="ui-select-choices"></div> </div> </div> ');
$templateCache.put('selectize/choices.tpl.html', '<div ng-show="$select.open" class="ui-select-choices selectize-dropdown single"> <div class="ui-select-choices-content selectize-dropdown-content"> <div class="ui-select-choices-row" ng-class="{\'active\': $select.activeIndex===$index}"> <div class="option" data-selectable ng-transclude></div> </div> </div> </div> ');
$templateCache.put('selectize/match.tpl.html', '<div ng-hide="$select.open || $select.selected===undefined" class="ui-select-match" ng-transclude></div> ');
Expand Down
15 changes: 15 additions & 0 deletions examples/bootstrap.html
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@
</div>
</div>

<div class="form-group">
<label class="col-sm-3 control-label">Tagging</label>
<div class="col-sm-6">

<ui-select ng-model="person.selected" theme="bootstrap" ng-disabled="false" tagging="tagging">
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="item in people | filter: $select.search">
<div ng-bind-html="item.name | highlight: $select.search"></div>
<small ng-bind-html="item.email | highlight: $select.search"></small>
</ui-select-choices>
</ui-select>

</div>
</div>

</fieldset>
</form>

Expand Down
4 changes: 4 additions & 0 deletions examples/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ app.controller('DemoCtrl', function($scope, $http) {
$scope.country.selected = undefined;
};

$scope.tagging = function(name) {
return {name: name, email: name + '@gamil.com', age: 'Unknown'};
};

$scope.person = {};
$scope.people = [
{ name: 'Adam', email: '[email protected]', age: 10 },
Expand Down
45 changes: 41 additions & 4 deletions src/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ angular.module('ui.select', [])
ctrl.disabled = undefined; // Initialized inside uiSelect directive link function
ctrl.resetSearchInput = undefined; // Initialized inside uiSelect 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) {
Expand Down Expand Up @@ -188,6 +189,10 @@ angular.module('ui.select', [])

// 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)
Expand Down Expand Up @@ -216,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:
Expand All @@ -235,7 +240,9 @@ angular.module('ui.select', [])
_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() {
Expand Down Expand Up @@ -280,7 +287,7 @@ angular.module('ui.select', [])
}])

.directive('uiSelect',
['$document', 'uiSelectConfig', 'uiSelectMinErr',
['$document', 'uiSelectConfig', 'uiSelectMinErr',
function($document, uiSelectConfig, uiSelectMinErr) {

return {
Expand Down Expand Up @@ -312,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);
Expand All @@ -322,6 +342,23 @@ angular.module('ui.select', [])
$select.selected = ngModel.$viewValue;
};

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;

Expand Down Expand Up @@ -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);
});

Expand Down
12 changes: 11 additions & 1 deletion test/select.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ describe('ui-select tests', function() {
if (attrs !== undefined) {
if (attrs.disabled !== undefined) { attrsHtml += ' ng-disabled="' + attrs.disabled + '"'; }
if (attrs.required !== undefined) { attrsHtml += ' ng-required="' + attrs.required + '"'; }
if (attrs.tagging !== undefined) { attrsHtml += ' tagging="' + attrs.tagging + '"'; }
}

return compileTemplate(
'<ui-select ng-model="selection"' + attrsHtml + '> \
'<ui-select ng-model="selection" ' + attrsHtml + '> \
<ui-select-match placeholder="Pick one...">{{$select.selected.name}}</ui-select-match> \
<ui-select-choices repeat="person in people | filter: $select.search"> \
<div ng-bind-html="person.name | highlight: $select.search"></div> \
Expand Down Expand Up @@ -163,6 +164,15 @@ describe('ui-select tests', function() {
expect(isDropdownOpened(el3)).toEqual(true);
});

it('should allow tagging if the attribute says so', function() {
var el = createUiSelect({tagging: true});
clickMatch(el);

$(el).scope().$select.select("I don't exist");

expect($(el).scope().$select.selected).toEqual("I don't exist");
});

// See when an item that evaluates to false (such as "false" or "no") is selected, the placeholder is shown https://github.com/angular-ui/ui-select/pull/32
it('should not display the placeholder when item evaluates to false', function() {
scope.items = ['false'];
Expand Down