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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 'adam@email.com', 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