<!DOCTYPE html>
<html lang="en" ng-app="demo">
<head>
<meta charset="utf-8">
<title>AngularJS ui-select</title>
<!--
IE8 support, see AngularJS Internet Explorer Compatibility http://docs.angularjs.org/guide/ie
For Firefox 3.6, you will also need to include jQuery and ECMAScript 5 shim
-->
<!--[if lt IE 9]>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/es5-shim/2.2.0/es5-shim.js"></script>
<script>
document.createElement('ui-select');
document.createElement('match');
document.createElement('choices');
</script>
<![endif]-->
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular-sanitize.js"></script>
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.css">
<!-- ui-select files -->
<script src="select.js"></script>
<link rel="stylesheet" href="select.css">
<style>
body {
padding: 15px;
}
</style>
</head>
<body ng-controller="DemoCtrl">
<script src="demo.js"></script>
<p>Selected: {{person.selected.name}}</p>
<ui-select ng-model="person.selected" theme="bootstrap">
<match placeholder="Select or search a person in the list...">{{$select.selected.name}}</match>
<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>
</choices>
</ui-select>
</body>
</html>
'use strict';
var app = angular.module('demo', ['ngSanitize', 'ui.select']);
/**
* AngularJS default filter with the following expression:
* "person in people | filter: {name: $select.search, age: $select.search}"
* performs a AND between 'name: $select.search' and 'age: $select.search'.
* We want to perform a OR.
*/
app.filter('propsFilter', function() {
return function(items, props) {
var out = [];
if (angular.isArray(items)) {
items.forEach(function(item) {
var itemMatches = false;
var keys = Object.keys(props);
for (var i = 0; i < keys.length; i++) {
var prop = keys[i];
var text = props[prop].toLowerCase();
if (item[prop].toString().toLowerCase().indexOf(text) !== -1) {
itemMatches = true;
break;
}
}
if (itemMatches) {
out.push(item);
}
});
} else {
// Let the output be the input untouched
out = items;
}
return out;
}
});
app.controller('DemoCtrl', function($scope, $http) {
$scope.disabled = undefined;
$scope.enable = function() {
$scope.disabled = false;
};
$scope.disable = function() {
$scope.disabled = true;
};
$scope.clear = function() {
$scope.person.selected = undefined;
};
$scope.person = {};
$scope.people = [
{ name: 'Adam', email: 'adam@email.com', age: 10 },
{ name: 'Amalie', email: 'amalie@email.com', age: 12 },
{ name: 'Wladimir', email: 'wladimir@email.com', age: 30 },
{ name: 'Samantha', email: 'samantha@email.com', age: 31 },
{ name: 'EstefanÃa', email: 'estefanÃa@email.com', age: 16 },
{ name: 'Natasha', email: 'natasha@email.com', age: 54 },
{ name: 'Nicole', email: 'nicole@email.com', age: 43 },
{ name: 'Adrian', email: 'adrian@email.com', age: 21 }
];
});
'use strict';
/**
* Add querySelectorAll() to jqLite.
*
* jqLite find() is limited to lookups by tag name.
* TODO This will change with future versions of AngularJS, to be removed when this happens
*
* See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586
* See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598
*/
if (angular.element.prototype.querySelectorAll === undefined) {
angular.element.prototype.querySelectorAll = function(selector) {
return angular.element(this[0].querySelectorAll(selector));
};
}
angular.module('ui.select', [])
.constant('uiSelectConfig', {
theme: 'bootstrap',
placeholder: '', // Empty by default, like HTML tag <select>
refreshDelay: 1000 // In milliseconds
})
// See Rename minErr and make it accessible from outside https://github.com/angular/angular.js/issues/6913
.service('uiSelectMinErr', function() {
var minErr = angular.$$minErr('ui.select');
return function() {
var error = minErr.apply(this, arguments);
var message = error.message.replace(new RegExp('\nhttp://errors.angularjs.org/.*'), '');
return new Error(message);
}
})
/**
* Parses "repeat" attribute.
*
* Taken from AngularJS ngRepeat source code
* See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L211
*
* Original discussion about parsing "repeat" attribute instead of fully relying on ng-repeat:
* https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697
*/
.service('RepeatParser', ['uiSelectMinErr', function(uiSelectMinErr) {
var self = this;
/**
* Example:
* expression = "address in addresses | filter: {street: $select.search} track by $index"
* lhs = "address",
* rhs = "addresses | filter: {street: $select.search}",
* trackByExp = "$index",
* valueIdentifier = "address",
* keyIdentifier = undefined
*/
self.parse = function(expression) {
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
if (!match) {
throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
expression);
}
var lhs = match[1]; // Left-hand side
var rhs = match[2]; // Right-hand side
var trackByExp = match[3];
match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
if (!match) {
throw uiSelectMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
lhs);
}
// Unused for now
var valueIdentifier = match[3] || match[1];
var keyIdentifier = match[2];
return {
lhs: lhs,
rhs: rhs,
trackByExp: trackByExp
};
};
self.getNgRepeatExpression = function(lhs, rhs, trackByExp) {
var expression = lhs + ' in ' + rhs;
if (trackByExp) {
expression += ' track by ' + trackByExp;
}
return expression;
};
}])
/**
* Contains ui-select "intelligence".
*
* The goal is to limit dependency on the DOM whenever possible and
* put as much logic in the controller (instead of the link functions) as possible so it can be easily tested.
*/
.controller('uiSelectCtrl',
['$scope', '$element', '$timeout', 'RepeatParser', 'uiSelectMinErr',
function($scope, $element, $timeout, RepeatParser, uiSelectMinErr) {
var ctrl = this;
var EMPTY_SEARCH = '';
ctrl.placeholder = undefined;
ctrl.search = EMPTY_SEARCH;
ctrl.activeIndex = 0;
ctrl.items = [];
ctrl.selected = undefined;
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
var _searchInput = $element.querySelectorAll('input.ui-select-search');
if (_searchInput.length !== 1) {
throw uiSelectMinErr('searchInput', "Expected 1 input.ui-select-search but got '{0}'.", _searchInput.length);
}
// Most of the time the user does not want to empty the search input when in typeahead mode
function _resetSearchInput() {
if (ctrl.resetSearchInput) {
ctrl.search = EMPTY_SEARCH;
}
}
// When the user clicks on ui-select, displays the dropdown list
ctrl.activate = function() {
if (!ctrl.disabled) {
_resetSearchInput();
ctrl.open = true;
// Give it time to appear before focus
$timeout(function() {
_searchInput[0].focus();
});
}
};
ctrl.parseRepeatAttr = function(repeatAttr) {
var repeat = RepeatParser.parse(repeatAttr);
// See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259
$scope.$watchCollection(repeat.rhs, function(items) {
if (items === undefined || items === null) {
// If the user specifies undefined or null => reset the collection
// Special case: items can be undefined if the user did not initialized the collection on the scope
// i.e $scope.addresses = [] is missing
ctrl.items = [];
} else {
if (!angular.isArray(items)) {
throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
} else {
// Regular case
ctrl.items = items;
}
}
});
};
var _refreshDelayPromise = undefined;
/**
* Typeahead mode: lets the user refresh the collection using his own function.
*
* See Expose $select.search for external / remote filtering https://github.com/angular-ui/ui-select/pull/31
*/
ctrl.refresh = function(refreshAttr) {
if (refreshAttr !== undefined) {
// Throttle / 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);
}, ctrl.refreshDelay);
}
};
// When the user clicks on an item inside the dropdown
ctrl.select = function(item) {
ctrl.selected = item;
ctrl.close();
// Using a watch instead of $scope.ngModel.$setViewValue(item)
};
// Closes the dropdown
ctrl.close = function() {
if (ctrl.open) {
_resetSearchInput();
ctrl.open = false;
}
};
var Key = {
Enter: 13,
Tab: 9,
Up: 38,
Down: 40,
Escape: 27
};
function _onKeydown(key) {
var processed = true;
switch (key) {
case Key.Down:
if (ctrl.activeIndex < ctrl.items.length - 1) { ctrl.activeIndex++; }
break;
case Key.Up:
if (ctrl.activeIndex > 0) { ctrl.activeIndex--; }
break;
case Key.Tab:
case Key.Enter:
ctrl.select(ctrl.items[ctrl.activeIndex]);
break;
case Key.Escape:
ctrl.close();
break;
default:
processed = false;
}
return processed;
}
// 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) {
var key = e.which;
$scope.$apply(function() {
var processed = _onKeydown(key);
if (processed) {
e.preventDefault();
e.stopPropagation();
}
});
switch (key) {
case Key.Down:
case Key.Up:
_ensureHighlightVisible();
break;
}
}
});
// See https://github.com/ivaynberg/select2/blob/3.4.6/select2.js#L1431
function _ensureHighlightVisible() {
var container = $element.querySelectorAll('.ui-select-choices-content');
var rows = container.querySelectorAll('.ui-select-choices-row');
if (rows.length < 1) {
throw uiSelectMinErr('rows', "Expected multiple .ui-select-choices-row but got '{0}'.", rows.length);
}
var highlighted = rows[ctrl.activeIndex];
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;
}
}
$scope.$on('$destroy', function() {
_searchInput.off('keydown');
});
}])
.directive('uiSelect',
['$document', 'uiSelectConfig',
function($document, uiSelectConfig) {
return {
restrict: 'EA',
templateUrl: function(tElement, tAttrs) {
var theme = tAttrs.theme || uiSelectConfig.theme;
return theme + '/select.tpl.html';
},
replace: true,
transclude: true,
require: ['uiSelect', 'ngModel'],
scope: true,
controller: 'uiSelectCtrl',
controllerAs: '$select',
link: function(scope, element, attrs, ctrls, transcludeFn) {
var $select = ctrls[0];
var ngModel = ctrls[1];
attrs.$observe('disabled', function() {
// No need to use $eval() (thanks to ng-disabled) since we already get a boolean instead of a string
$select.disabled = attrs.disabled !== undefined ? attrs.disabled : false;
});
attrs.$observe('resetSearchInput', function() {
// $eval() is needed otherwise we get a string instead of a boolean
var resetSearchInput = scope.$eval(attrs.resetSearchInput);
$select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true;
});
scope.$watch('$select.selected', function(newValue, oldValue) {
if (ngModel.$viewValue !== newValue) {
ngModel.$setViewValue(newValue);
}
});
ngModel.$render = function() {
$select.selected = ngModel.$viewValue;
};
// See Click everywhere but here event http://stackoverflow.com/questions/12931369
$document.on('mousedown', function(e) {
var contains = false;
if (window.jQuery) {
// Firefox 3.6 does not support element.contains()
// See Node.contains https://developer.mozilla.org/en-US/docs/Web/API/Node.contains
contains = window.jQuery.contains(element[0], e.target);
} else {
contains = element[0].contains(e.target);
}
if (!contains) {
$select.close();
scope.$digest();
}
});
scope.$on('$destroy', function() {
$document.off('mousedown');
});
// Move transcluded elements to their correct position in main template
transcludeFn(scope, function(clone) {
// See Transclude in AngularJS http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html
// One day jqLite will be replaced by jQuery and we will be able to write:
// var transcludedElement = clone.filter('.my-class')
// instead of creating a hackish DOM element:
var transcluded = angular.element('<div>').append(clone);
var transcludedMatch = transcluded.querySelectorAll('.ui-select-match');
if (transcludedMatch.length !== 1) {
throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-match but got '{0}'.", transcludedMatch.length);
}
element.querySelectorAll('.ui-select-match').replaceWith(transcludedMatch);
var transcludedChoices = transcluded.querySelectorAll('.ui-select-choices');
if (transcludedChoices.length !== 1) {
throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-choices but got '{0}'.", transcludedChoices.length);
}
element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
});
}
};
}])
.directive('choices',
['uiSelectConfig', 'RepeatParser', 'uiSelectMinErr',
function(uiSelectConfig, RepeatParser, uiSelectMinErr) {
return {
restrict: 'EA',
require: '^uiSelect',
replace: true,
transclude: true,
templateUrl: function(tElement) {
// Gets theme attribute from parent (ui-select)
var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
return theme + '/choices.tpl.html';
},
compile: function(tElement, tAttrs) {
var repeat = RepeatParser.parse(tAttrs.repeat);
var rows = tElement.querySelectorAll('.ui-select-choices-row');
if (rows.length !== 1) {
throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", rows.length);
}
rows.attr('ng-repeat', RepeatParser.getNgRepeatExpression(repeat.lhs, '$select.items', repeat.trackByExp))
.attr('ng-mouseenter', '$select.activeIndex = $index')
.attr('ng-click', '$select.select(' + repeat.lhs + ')');
return function link(scope, element, attrs, $select) {
$select.parseRepeatAttr(attrs.repeat);
scope.$watch('$select.search', function() {
$select.activeIndex = 0;
$select.refresh(attrs.refresh);
});
attrs.$observe('refreshDelay', function() {
// $eval() is needed otherwise we get a string instead of a number
var refreshDelay = scope.$eval(attrs.refreshDelay);
$select.refreshDelay = refreshDelay !== undefined ? refreshDelay : uiSelectConfig.refreshDelay;
});
};
}
};
}])
.directive('match', ['uiSelectConfig', function(uiSelectConfig) {
return {
restrict: 'EA',
require: '^uiSelect',
replace: true,
transclude: true,
templateUrl: function(tElement) {
// Gets theme attribute from parent (ui-select)
var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
return theme + '/match.tpl.html';
},
link: function(scope, element, attrs, $select) {
attrs.$observe('placeholder', function(placeholder) {
$select.placeholder = placeholder !== undefined ? placeholder : uiSelectConfig.placeholder;
});
}
};
}])
/**
* Highlights text that matches $select.search.
*
* Taken from AngularUI Bootstrap Typeahead
* See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L340
*/
.filter('highlight', function() {
function escapeRegexp(queryToEscape) {
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
}
return function(matchItem, query) {
return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<span class="ui-select-highlight">$&</span>') : matchItem;
};
});
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/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/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> ');
$templateCache.put('selectize/select.tpl.html', '<div class="selectize-control single"> <div class="selectize-input" ng-class="{\'focus\': $select.open, \'disabled\': $select.disabled}" ng-click="$select.activate()"> <div class="ui-select-match"></div> <input type="text" autocomplete="off" tabindex="" class="ui-select-search" placeholder="{{$select.placeholder}}" ng-model="$select.search" ng-hide="$select.selected && !$select.open" ng-disabled="$select.disabled"> </div> <div class="ui-select-choices"></div> </div> ');
}]);
/* Style when highlighting a search. */
.ui-select-highlight {
font-weight: bold;
}
/* Select2 theme */
/* Selectize theme */
/* Fix input width for Selectize theme */
.selectize-control > .selectize-input > input {
width: 100%;
}
/* Fix dropdown width for Selectize theme */
.selectize-control > .selectize-dropdown {
width: 100%;
}
/* Bootstrap theme */
/* Fix Bootstrap dropdown position when inside a input-group */
.input-group > .ui-select-bootstrap.dropdown {
/* Instead of relative */
position: static;
}
.input-group > .ui-select-bootstrap > input.ui-select-search.form-control {
border-radius: 4px; /* FIXME hardcoded value :-/ */
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.ui-select-bootstrap > .ui-select-match {
/* Instead of center because of .btn */
text-align: left;
}
.ui-select-bootstrap > .ui-select-match > .caret {
position: absolute;
top: 45%;
right: 15px;
}
.ui-select-bootstrap > .ui-select-choices {
width: 100%;
}
/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */
.ui-select-bootstrap > .ui-select-choices {
height: auto;
max-height: 200px;
overflow-x: hidden;
}