diff --git a/package.json b/package.json
index 699f687f..7b25166b 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,6 @@
"repository": "ovh-ux/ovh-ui-angular",
"main": "./dist/oui-angular.min.js",
"browser": "./dist/oui-angular.min.js",
- "module": "./packages/oui-angular/src/index.js",
"scripts": {
"commit": "npm-scripts-config commit",
"commitmsg": "npm-scripts-config commitmsg",
@@ -35,18 +34,25 @@
}
},
"dependencies": {
+ "bloodhound-js": "^1.2.3",
+ "clipboard": "^2.0.1",
+ "escape-string-regexp": "^1.0.5",
+ "flatpickr": "^4.5.2",
+ "popper.js": "^1.14.4",
+ "ui-select": "^0.19.8"
+ },
+ "peerDependencies": {
"angular": ">=1.6.x",
"angular-aria": ">=1.6.x",
"angular-sanitize": ">=1.6.x",
- "clipboard": "^2.0.1",
- "escape-string-regexp": "^1.0.5",
"flatpickr": "^4.5.2",
- "ovh-ui-kit": "^2.20.0",
- "popper.js": "^1.14.4"
+ "ovh-ui-kit": "^2.20.0"
},
"devDependencies": {
+ "angular": ">=1.6.x",
+ "angular-aria": ">=1.6.x",
+ "angular-sanitize": ">=1.6.x",
"angular-mocks": ">=1.6.x",
- "autoprefixer": "^9.1.5",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^9.0.0",
@@ -59,15 +65,11 @@
"babel-preset-stage-2": "^6.24.1",
"babel-register": "^6.26.0",
"cross-env": "^5.2.0",
- "css-loader": "^1.0.0",
"eslint": "^4.3.0",
"eslint-config-ovh": "^0.1.1",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.1.1",
- "eventsource-polyfill": "^0.9.6",
- "express": "^4.16.3",
"html-loader": "^0.5.5",
- "html-webpack-plugin": "^3.2.0",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.2.1",
"karma": "^3.0.0",
@@ -80,29 +82,17 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "^0.0.32",
"karma-webpack": "^4.0.0-rc.2",
- "less": "^3.8.1",
- "less-loader": "^4.1.0",
- "loader-utils": "^1.1.0",
"lodash": "^4.17.11",
"lodash-webpack-plugin": "^0.11.5",
- "marked": "^0.5.0",
- "minimist": "^1.2.0",
"npm-scripts-config": "^0.0.2",
"npm-scripts-conventional-changelog": "^0.1.0",
- "opn": "^5.3.0",
- "portscanner": "^2.2.0",
- "postcss-loader": "^3.0.0",
"rimraf": "^2.6.2",
- "style-loader": "^0.23.0",
"webpack": "^4.19.1",
"webpack-cli": "^3.1.0",
"webpack-dev-middleware": "^3.3.0",
"webpack-hot-middleware": "^2.24.0",
"webpack-merge": "^4.1.4"
},
- "peerDependencies": {
- "ovh-ui-kit": "*"
- },
"engines": {
"node": ">= 6.9.0",
"npm": ">= 3.10.0",
diff --git a/packages/oui-angular/src/index.js b/packages/oui-angular/src/index.js
index c32f6756..62b6ec9d 100644
--- a/packages/oui-angular/src/index.js
+++ b/packages/oui-angular/src/index.js
@@ -1,4 +1,5 @@
import ActionMenu from "@ovh-ui/oui-action-menu";
+import Autocomplete from "@ovh-ui/oui-autocomplete";
import BackButton from "@ovh-ui/oui-back-button";
import Button from "@ovh-ui/oui-button";
import Calendar from "@ovh-ui/oui-calendar";
@@ -10,7 +11,9 @@ import CriteriaAdder from "@ovh-ui/oui-criteria-adder";
import CriteriaContainer from "@ovh-ui/oui-criteria-container";
import Datagrid from "@ovh-ui/oui-datagrid";
import Dropdown from "@ovh-ui/oui-dropdown";
+import DualList from "@ovh-ui/oui-dual-list";
import Field from "@ovh-ui/oui-field";
+import File from "@ovh-ui/oui-file";
import FormActions from "@ovh-ui/oui-form-actions";
import GuideMenu from "@ovh-ui/oui-guide-menu";
import HeaderTabs from "@ovh-ui/oui-header-tabs";
@@ -32,6 +35,7 @@ import Slideshow from "@ovh-ui/oui-slideshow";
import Spinner from "@ovh-ui/oui-spinner";
import Stepper from "@ovh-ui/oui-stepper";
import Switch from "@ovh-ui/oui-switch";
+import Tabs from "@ovh-ui/oui-tabs";
import Textarea from "@ovh-ui/oui-textarea";
import Tile from "@ovh-ui/oui-tile";
import Tooltip from "@ovh-ui/oui-tooltip";
@@ -39,6 +43,7 @@ import Tooltip from "@ovh-ui/oui-tooltip";
export default angular
.module("oui", [
ActionMenu,
+ Autocomplete,
BackButton,
Button,
Calendar,
@@ -50,7 +55,9 @@ export default angular
CriteriaContainer,
Datagrid,
Dropdown,
+ DualList,
Field,
+ File,
FormActions,
GuideMenu,
HeaderTabs,
@@ -72,6 +79,7 @@ export default angular
Spinner,
Stepper,
Switch,
+ Tabs,
Textarea,
Tile,
Tooltip
diff --git a/packages/oui-angular/src/index.spec.js b/packages/oui-angular/src/index.spec.js
index 67b46a72..d62f271c 100644
--- a/packages/oui-angular/src/index.spec.js
+++ b/packages/oui-angular/src/index.spec.js
@@ -1,6 +1,7 @@
import "@ovh-ui/common/test-utils";
loadTests(require.context("../../oui-action-menu/src/", true, /.*((\.spec)|(index))$/));
+loadTests(require.context("../../oui-autocomplete/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-back-button/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-button/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-calendar/src/", true, /.*((\.spec)|(index))$/));
@@ -12,7 +13,9 @@ loadTests(require.context("../../oui-criteria-adder/src/", true, /.*((\.spec)|(i
loadTests(require.context("../../oui-criteria-container/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-datagrid/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-dropdown/src/", true, /.*((\.spec)|(index))$/));
+loadTests(require.context("../../oui-dual-list/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-field/src/", true, /.*((\.spec)|(index))$/));
+loadTests(require.context("../../oui-file/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-form-actions/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-guide-menu/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-header-tabs/src/", true, /.*((\.spec)|(index))$/));
@@ -34,6 +37,7 @@ loadTests(require.context("../../oui-slideshow/src/", true, /.*((\.spec)|(index)
loadTests(require.context("../../oui-spinner/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-stepper/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-switch/src/", true, /.*((\.spec)|(index))$/));
+loadTests(require.context("../../oui-tabs/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-tile/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-textarea/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-tooltip/src/", true, /.*((\.spec)|(index))$/));
diff --git a/packages/oui-autocomplete/README.md b/packages/oui-autocomplete/README.md
new file mode 100644
index 00000000..b158c16c
--- /dev/null
+++ b/packages/oui-autocomplete/README.md
@@ -0,0 +1,83 @@
+# Autocomplete
+
+
+
+## Usage
+
+### Basic
+
+#### Array of strings
+
+```html:preview
+
+```
+
+#### Array of objects
+
+```html:preview
+
+```
+
+### Events
+
+**Note**: If you want to access the parameters inside `on-select` callback, you need to use `value` variable as below.
+It will return the corresponding value from your array of suggestions.
+
+```html:preview
+
+
+
model value: {{$ctrl.modelOnSelect | json}}
+
onSelect 'value' value: {{$ctrl.selectedValue | json}}
+
+```
+
+## Variants
+
+### Search
+
+See [Search](#!/oui-angular/search) component.
+
+```html:preview
+
+
+```
+
+## API
+
+| Attribute | Type | Binding | One-time binding | Values | Default | Description
+| ---- | ---- | ---- | ---- | ---- | ---- | ----
+| `oui-autocomplete` | array | < | no | n/a | n/a | array of suggestions
+| `oui-autocomplete-options` | object | | yes | n/a | n/a | options of autocomplete
+| `oui-autocomplete-property` | string | @? | no | n/a | n/a | property path used to get value from suggestion
+| `oui-autocomplete-on-select` | function | & | no | n/a | n/a | handler triggered when suggestion is selected
+
+## Configuration
+
+The autocomplete can be globally configured with a provider.
+
+```js
+angular.module("myModule", [
+ "oui.autocomplete"
+]).config(ouiAutocompleteConfigurationProvider => {
+ ouiAutocompleteConfigurationProvider.setOptions({ // default options
+ debounceDelay: 500,
+ minLength: 2
+ });
+});
+```
diff --git a/packages/oui-autocomplete/package.json b/packages/oui-autocomplete/package.json
new file mode 100644
index 00000000..07ca9082
--- /dev/null
+++ b/packages/oui-autocomplete/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ovh-ui/oui-autocomplete",
+ "version": "1.0.0",
+ "main": "./src/index.js",
+ "license": "BSD-3-Clause",
+ "author": "OVH SAS",
+ "dependencies": {
+ "bloodhound-js": "^1.2.3",
+ "popper.js": "^1.14.4"
+ }
+}
diff --git a/packages/oui-autocomplete/src/autocomplete.controller.js b/packages/oui-autocomplete/src/autocomplete.controller.js
new file mode 100644
index 00000000..7e5b7641
--- /dev/null
+++ b/packages/oui-autocomplete/src/autocomplete.controller.js
@@ -0,0 +1,251 @@
+import Bloodhound from "bloodhound-js";
+import debounce from "lodash/debounce";
+import get from "lodash/get";
+import merge from "lodash/merge";
+import Popper from "popper.js";
+import template from "./autocomplete.html";
+
+const KEYBOARD_KEYS = {
+ TAB: 9,
+ SHIFT: 16,
+ ESC: 27,
+ UP: 38,
+ DOWN: 40
+};
+
+export default class {
+ constructor ($compile, $document, $element, $scope, $timeout, ouiAutocompleteConfiguration) {
+ "ngInject";
+
+ this.$compile = $compile;
+ this.$document = $document;
+ this.$element = $element;
+ this.$timeout = $timeout;
+ this.$scope = $scope;
+ this.providerOptions = ouiAutocompleteConfiguration.options;
+ }
+
+ createDatalist () {
+ const input = this.$element[0];
+ const autocomplete = this.$element.next()[0];
+
+ // Let Popper.js position the datalist
+ this.popper = new Popper(input, autocomplete, {
+ placement: "bottom-start"
+ });
+
+ this.triggerWidth = `${this.popper.reference.clientWidth}px`;
+ }
+
+ openDatalist (datum) {
+ if (!datum.length) {
+ this.closeDatalist();
+ return;
+ }
+
+ this.$timeout(() => {
+ // Refresh keyboard navigation
+ this.navItems = undefined;
+ this.navIndex = undefined;
+
+ this.datalist = datum;
+ this.isOpen = true;
+
+ // Init keyboard navigation
+ if (!this.isNavigable) {
+ this.isNavigable = true;
+ this.$document
+ .one("click", () => this.closeDatalist())
+ .on("keydown", (e) => this.triggerKeyHandler(e))
+ .on("keyup", (e) => delete this.keys[e.which]);
+ }
+ });
+ }
+
+ closeDatalist () {
+ this.$timeout(() => {
+ this.datalist = [];
+ this.isOpen = false;
+
+ // Clear keyboard navigation
+ if (this.isNavigable) {
+ this.isNavigable = false;
+ this.keys = {};
+
+ this.$document
+ .off("click")
+ .off("keydown")
+ .off("keyup");
+ }
+ });
+ }
+
+ initSearchEngine (suggestions) {
+ if (!this.engine && angular.isArray(suggestions)) {
+ this.engine = new Bloodhound({
+ local: suggestions,
+ datumTokenizer: (datum) => {
+ const value = this.getProperty(datum);
+ return Bloodhound.tokenizers.whitespace(value);
+ },
+ queryTokenizer: Bloodhound.tokenizers.whitespace
+ });
+
+ this.engine.initialize();
+
+ // Watch model value for search engine
+ this.$scope.$watch(
+ () => this.model.$modelValue,
+ debounce(
+ (value) => this.searchQuery(value),
+ this.options.debounceDelay
+ )
+ );
+ }
+ }
+
+ updateSearchEngine (suggestions) {
+ if (this.engine && angular.isArray(suggestions)) {
+ this.engine.clear();
+ this.engine.local = suggestions;
+ this.engine.initialize(true);
+ }
+ }
+
+ searchQuery (query) {
+ if (angular.isString(query) && query !== this.selectedValue) {
+ this.$timeout(() => {
+ if (query.length >= this.options.minLength) {
+ this.engine.search(
+ query,
+ (datum) => this.openDatalist(datum), // Sync
+ (datum) => this.openDatalist(datum) // Async
+ );
+ } else if (this.isOpen) {
+ this.closeDatalist();
+ }
+
+ // Needed for highlight filter
+ this.query = query;
+ });
+ }
+ }
+
+ updateValue (value) {
+ this.$element[0].focus();
+ this.selectedValue = this.getProperty(value);
+ this.closeDatalist();
+
+ // Update value and notify model change
+ this.model.$setViewValue(this.selectedValue);
+ this.model.$render();
+
+ // Callback
+ this.onSelect({
+ value: angular.copy(value) // Clean $$hashKey
+ });
+ }
+
+ getProperty (item) {
+ return get(item, this.property, item);
+ }
+
+ focusNavItem (direction) {
+ if (angular.isUndefined(this.navItems)) {
+ this.navItems = this.autocomplete.find("button");
+ this.navItems.push(this.$element[0]);
+ this.navLastIndex = this.navItems.length - 1;
+ }
+
+ // Set index of trigger input if undefined
+ if (angular.isUndefined(this.navIndex)) {
+ this.navIndex = this.navLastIndex;
+ }
+
+ if (direction === "next") {
+ this.navIndex = this.navIndex >= this.navLastIndex ? 0 : this.navIndex + 1;
+ } else if (direction === "prev") {
+ this.navIndex = this.navIndex <= 0 ? this.navLastIndex : this.navIndex - 1;
+ }
+
+ this.navItems[this.navIndex].focus();
+ }
+
+ triggerKeyHandler (e) {
+ if (this.isNavigable) {
+ const key = e.which;
+
+ if ([
+ KEYBOARD_KEYS.TAB,
+ KEYBOARD_KEYS.SHIFT,
+ KEYBOARD_KEYS.UP,
+ KEYBOARD_KEYS.DOWN,
+ KEYBOARD_KEYS.ESC
+ ].indexOf(key) > -1) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Add key in array for key combination
+ this.keys[key] = true;
+
+ if (
+ (this.keys[KEYBOARD_KEYS.TAB] && !this.keys[KEYBOARD_KEYS.SHIFT]) ||
+ this.keys[KEYBOARD_KEYS.DOWN]
+ ) {
+ // Move Down
+ this.focusNavItem("next");
+ } else if (
+ (this.keys[KEYBOARD_KEYS.TAB] && this.keys[KEYBOARD_KEYS.SHIFT]) ||
+ this.keys[KEYBOARD_KEYS.UP]
+ ) {
+ // Move Up
+ this.focusNavItem("prev");
+ } else if (this.keys[KEYBOARD_KEYS.ESC]) {
+ // Escape
+ this.closeDatalist();
+ }
+ }
+ }
+ }
+
+ $onChanges (changes) {
+ if (changes.suggestions && changes.suggestions.currentValue) {
+ this.updateSearchEngine(changes.suggestions.currentValue);
+ }
+ }
+
+ $onInit () {
+ this.id = `ouiAutocomplete${this.$scope.$id}`;
+ this.options = merge(this.providerOptions, this.options);
+ this.keys = {};
+
+ this.initSearchEngine(this.suggestions);
+ }
+
+ $postLink () {
+ this.$timeout(() => {
+ // Create a new scope to compile the autocomplete next to the input
+ const autocompleteScope = angular.extend(this.$scope.$new(true), { $autocompleteCtrl: this });
+ this.autocomplete = this.$compile(template)(autocompleteScope);
+
+ this.$element
+ .attr("autocomplete", "off")
+ .attr("list", this.id)
+ .one("focus", () => this.createDatalist()) // One time bind to create the popper helper
+ .on("click", (e) => e.stopPropagation()) // Avoid click propagation on $element
+ .after(this.autocomplete); // Add compiled template after $element
+ });
+ }
+
+ $onDestroy () {
+ if (this.engine) {
+ this.engine.clear();
+ }
+
+ if (this.isNavigable) {
+ this.$element
+ .off("click")
+ .off("keydown");
+ }
+ }
+}
diff --git a/packages/oui-autocomplete/src/autocomplete.directive.js b/packages/oui-autocomplete/src/autocomplete.directive.js
new file mode 100644
index 00000000..6f0a6341
--- /dev/null
+++ b/packages/oui-autocomplete/src/autocomplete.directive.js
@@ -0,0 +1,21 @@
+import controller from "./autocomplete.controller";
+
+export default () => {
+ "ngInject";
+
+ return {
+ restrict: "A",
+ require: {
+ model: "ngModel"
+ },
+ scope: true,
+ bindToController: {
+ suggestions: "
+
+
+
+
diff --git a/packages/oui-autocomplete/src/autocomplete.provider.js b/packages/oui-autocomplete/src/autocomplete.provider.js
new file mode 100644
index 00000000..d804b7e7
--- /dev/null
+++ b/packages/oui-autocomplete/src/autocomplete.provider.js
@@ -0,0 +1,25 @@
+import merge from "lodash/merge";
+
+export default class {
+ constructor () {
+ this.options = {
+ debounceDelay: 250,
+ minLength: 2
+ };
+ }
+
+ /**
+ * Set the options of the flatpickr calendar
+ * @param {Object} options the configuration of the calendar
+ */
+ setOptions (options) {
+ this.options = merge(this.options, options);
+ return this;
+ }
+
+ $get () {
+ return angular.copy({
+ options: this.options
+ });
+ }
+}
diff --git a/packages/oui-autocomplete/src/filter/autocomplete-highlight.filter.js b/packages/oui-autocomplete/src/filter/autocomplete-highlight.filter.js
new file mode 100644
index 00000000..aff5f293
--- /dev/null
+++ b/packages/oui-autocomplete/src/filter/autocomplete-highlight.filter.js
@@ -0,0 +1,16 @@
+export default ($sce) => {
+ "ngInject";
+
+ const highlight = '$&';
+ const escapeRegexp = (query) =>
+ query.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
+
+ // Highlight query in the beginning of each matching words
+ return (matchItem, query) => {
+ const regexp = `\\b${escapeRegexp(query)}`;
+ return $sce.trustAsHtml(query ?
+ matchItem.replace(new RegExp(regexp, "gi"), highlight) :
+ matchItem
+ );
+ };
+};
diff --git a/packages/oui-autocomplete/src/index.js b/packages/oui-autocomplete/src/index.js
new file mode 100644
index 00000000..ad0f52d3
--- /dev/null
+++ b/packages/oui-autocomplete/src/index.js
@@ -0,0 +1,10 @@
+import Autocomplete from "./autocomplete.directive";
+import AutocompleteHightlight from "./filter/autocomplete-highlight.filter";
+import AutocompleteProvider from "./autocomplete.provider";
+
+export default angular
+ .module("oui.autocomplete", [])
+ .directive("ouiAutocomplete", Autocomplete)
+ .filter("ouiAutocompleteHightlight", AutocompleteHightlight)
+ .provider("ouiAutocompleteConfiguration", AutocompleteProvider)
+ .name;
diff --git a/packages/oui-autocomplete/src/index.spec.data.json b/packages/oui-autocomplete/src/index.spec.data.json
new file mode 100644
index 00000000..6d06b505
--- /dev/null
+++ b/packages/oui-autocomplete/src/index.spec.data.json
@@ -0,0 +1,492 @@
+{
+ "strings": [
+ "Afghanistan",
+ "Åland Islands",
+ "Albania",
+ "Algeria",
+ "American Samoa",
+ "AndorrA",
+ "Angola",
+ "Anguilla",
+ "Antarctica",
+ "Antigua and Barbuda",
+ "Argentina",
+ "Armenia",
+ "Aruba",
+ "Australia",
+ "Austria",
+ "Azerbaijan",
+ "Bahamas",
+ "Bahrain",
+ "Bangladesh",
+ "Barbados",
+ "Belarus",
+ "Belgium",
+ "Belize",
+ "Benin",
+ "Bermuda",
+ "Bhutan",
+ "Bolivia",
+ "Bosnia and Herzegovina",
+ "Botswana",
+ "Bouvet Island",
+ "Brazil",
+ "British Indian Ocean Territory",
+ "Brunei Darussalam",
+ "Bulgaria",
+ "Burkina Faso",
+ "Burundi",
+ "Cambodia",
+ "Cameroon",
+ "Canada",
+ "Cape Verde",
+ "Cayman Islands",
+ "Central African Republic",
+ "Chad",
+ "Chile",
+ "China",
+ "Christmas Island",
+ "Cocos (Keeling) Islands",
+ "Colombia",
+ "Comoros",
+ "Congo",
+ "Congo, The Democratic Republic of the",
+ "Cook Islands",
+ "Costa Rica",
+ "Cote D'Ivoire",
+ "Croatia",
+ "Cuba",
+ "Cyprus",
+ "Czech Republic",
+ "Denmark",
+ "Djibouti",
+ "Dominica",
+ "Dominican Republic",
+ "Ecuador",
+ "Egypt",
+ "El Salvador",
+ "Equatorial Guinea",
+ "Eritrea",
+ "Estonia",
+ "Ethiopia",
+ "Falkland Islands (Malvinas",
+ "Faroe Islands",
+ "Fiji",
+ "Finland",
+ "France",
+ "French Guiana",
+ "French Polynesia",
+ "French Southern Territories",
+ "Gabon",
+ "Gambia",
+ "Georgia",
+ "Germany",
+ "Ghana",
+ "Gibraltar",
+ "Greece",
+ "Greenland",
+ "Grenada",
+ "Guadeloupe",
+ "Guam",
+ "Guatemala",
+ "Guernsey",
+ "Guinea",
+ "Guinea-Bissau",
+ "Guyana",
+ "Haiti",
+ "Heard Island and Mcdonald Islands",
+ "Holy See (Vatican City State",
+ "Honduras",
+ "Hong Kong",
+ "Hungary",
+ "Iceland",
+ "India",
+ "Indonesia",
+ "Iran, Islamic Republic Of",
+ "Iraq",
+ "Ireland",
+ "Isle of Man",
+ "Israel",
+ "Italy",
+ "Jamaica",
+ "Japan",
+ "Jersey",
+ "Jordan",
+ "Kazakhstan",
+ "Kenya",
+ "Kiribati",
+ "Korea, Democratic People's Republic of",
+ "Korea, Republic of",
+ "Kuwait",
+ "Kyrgyzstan",
+ "Lao People's Democratic Republic",
+ "Latvia",
+ "Lebanon",
+ "Lesotho",
+ "Liberia",
+ "Libyan Arab Jamahiriya",
+ "Liechtenstein",
+ "Lithuania",
+ "Luxembourg",
+ "Macao",
+ "Macedonia, The Former Yugoslav Republic of",
+ "Madagascar",
+ "Malawi",
+ "Malaysia",
+ "Maldives",
+ "Mali",
+ "Malta",
+ "Marshall Islands",
+ "Martinique",
+ "Mauritania",
+ "Mauritius",
+ "Mayotte",
+ "Mexico",
+ "Micronesia, Federated States of",
+ "Moldova, Republic of",
+ "Monaco",
+ "Mongolia",
+ "Montserrat",
+ "Morocco",
+ "Mozambique",
+ "Myanmar",
+ "Namibia",
+ "Nauru",
+ "Nepal",
+ "Netherlands",
+ "Netherlands Antilles",
+ "New Caledonia",
+ "New Zealand",
+ "Nicaragua",
+ "Niger",
+ "Nigeria",
+ "Niue",
+ "Norfolk Island",
+ "Northern Mariana Islands",
+ "Norway",
+ "Oman",
+ "Pakistan",
+ "Palau",
+ "Palestinian Territory, Occupied",
+ "Panama",
+ "Papua New Guinea",
+ "Paraguay",
+ "Peru",
+ "Philippines",
+ "Pitcairn",
+ "Poland",
+ "Portugal",
+ "Puerto Rico",
+ "Qatar",
+ "Reunion",
+ "Romania",
+ "Russian Federation",
+ "RWANDA",
+ "Saint Helena",
+ "Saint Kitts and Nevis",
+ "Saint Lucia",
+ "Saint Pierre and Miquelon",
+ "Saint Vincent and the Grenadines",
+ "Samoa",
+ "San Marino",
+ "Sao Tome and Principe",
+ "Saudi Arabia",
+ "Senegal",
+ "Serbia and Montenegro",
+ "Seychelles",
+ "Sierra Leone",
+ "Singapore",
+ "Slovakia",
+ "Slovenia",
+ "Solomon Islands",
+ "Somalia",
+ "South Africa",
+ "South Georgia and the South Sandwich Islands",
+ "Spain",
+ "Sri Lanka",
+ "Sudan",
+ "Suriname",
+ "Svalbard and Jan Mayen",
+ "Swaziland",
+ "Sweden",
+ "Switzerland",
+ "Syrian Arab Republic",
+ "Taiwan, Province of China",
+ "Tajikistan",
+ "Tanzania, United Republic of",
+ "Thailand",
+ "Timor-Leste",
+ "Togo",
+ "Tokelau",
+ "Tonga",
+ "Trinidad and Tobago",
+ "Tunisia",
+ "Turkey",
+ "Turkmenistan",
+ "Turks and Caicos Islands",
+ "Tuvalu",
+ "Uganda",
+ "Ukraine",
+ "United Arab Emirates",
+ "United Kingdom",
+ "United States",
+ "United States Minor Outlying Islands",
+ "Uruguay",
+ "Uzbekistan",
+ "Vanuatu",
+ "Venezuela",
+ "Viet Nam",
+ "Virgin Islands, British",
+ "Virgin Islands, U.S",
+ "Wallis and Futuna",
+ "Western Sahara",
+ "Yemen",
+ "Zambia",
+ "Zimbabwe"
+ ],
+ "objects": [
+ { "country": { "name": "Afghanistan", "code": "AF" } },
+ { "country": { "name": "Åland Islands", "code": "AX" } },
+ { "country": { "name": "Albania", "code": "AL" } },
+ { "country": { "name": "Algeria", "code": "DZ" } },
+ { "country": { "name": "American Samoa", "code": "AS" } },
+ { "country": { "name": "AndorrA", "code": "AD" } },
+ { "country": { "name": "Angola", "code": "AO" } },
+ { "country": { "name": "Anguilla", "code": "AI" } },
+ { "country": { "name": "Antarctica", "code": "AQ" } },
+ { "country": { "name": "Antigua and Barbuda", "code": "AG" } },
+ { "country": { "name": "Argentina", "code": "AR" } },
+ { "country": { "name": "Armenia", "code": "AM" } },
+ { "country": { "name": "Aruba", "code": "AW" } },
+ { "country": { "name": "Australia", "code": "AU" } },
+ { "country": { "name": "Austria", "code": "AT" } },
+ { "country": { "name": "Azerbaijan", "code": "AZ" } },
+ { "country": { "name": "Bahamas", "code": "BS" } },
+ { "country": { "name": "Bahrain", "code": "BH" } },
+ { "country": { "name": "Bangladesh", "code": "BD" } },
+ { "country": { "name": "Barbados", "code": "BB" } },
+ { "country": { "name": "Belarus", "code": "BY" } },
+ { "country": { "name": "Belgium", "code": "BE" } },
+ { "country": { "name": "Belize", "code": "BZ" } },
+ { "country": { "name": "Benin", "code": "BJ" } },
+ { "country": { "name": "Bermuda", "code": "BM" } },
+ { "country": { "name": "Bhutan", "code": "BT" } },
+ { "country": { "name": "Bolivia", "code": "BO" } },
+ { "country": { "name": "Bosnia and Herzegovina", "code": "BA" } },
+ { "country": { "name": "Botswana", "code": "BW" } },
+ { "country": { "name": "Bouvet Island", "code": "BV" } },
+ { "country": { "name": "Brazil", "code": "BR" } },
+ { "country": { "name": "British Indian Ocean Territory", "code": "IO" } },
+ { "country": { "name": "Brunei Darussalam", "code": "BN" } },
+ { "country": { "name": "Bulgaria", "code": "BG" } },
+ { "country": { "name": "Burkina Faso", "code": "BF" } },
+ { "country": { "name": "Burundi", "code": "BI" } },
+ { "country": { "name": "Cambodia", "code": "KH" } },
+ { "country": { "name": "Cameroon", "code": "CM" } },
+ { "country": { "name": "Canada", "code": "CA" } },
+ { "country": { "name": "Cape Verde", "code": "CV" } },
+ { "country": { "name": "Cayman Islands", "code": "KY" } },
+ { "country": { "name": "Central African Republic", "code": "CF" } },
+ { "country": { "name": "Chad", "code": "TD" } },
+ { "country": { "name": "Chile", "code": "CL" } },
+ { "country": { "name": "China", "code": "CN" } },
+ { "country": { "name": "Christmas Island", "code": "CX" } },
+ { "country": { "name": "Cocos (Keeling) Islands", "code": "CC" } },
+ { "country": { "name": "Colombia", "code": "CO" } },
+ { "country": { "name": "Comoros", "code": "KM" } },
+ { "country": { "name": "Congo", "code": "CG" } },
+ { "country": { "name": "Congo, The Democratic Republic of the", "code": "CD" } },
+ { "country": { "name": "Cook Islands", "code": "CK" } },
+ { "country": { "name": "Costa Rica", "code": "CR" } },
+ { "country": { "name": "Cote D'Ivoire", "code": "CI" } },
+ { "country": { "name": "Croatia", "code": "HR" } },
+ { "country": { "name": "Cuba", "code": "CU" } },
+ { "country": { "name": "Cyprus", "code": "CY" } },
+ { "country": { "name": "Czech Republic", "code": "CZ" } },
+ { "country": { "name": "Denmark", "code": "DK" } },
+ { "country": { "name": "Djibouti", "code": "DJ" } },
+ { "country": { "name": "Dominica", "code": "DM" } },
+ { "country": { "name": "Dominican Republic", "code": "DO" } },
+ { "country": { "name": "Ecuador", "code": "EC" } },
+ { "country": { "name": "Egypt", "code": "EG" } },
+ { "country": { "name": "El Salvador", "code": "SV" } },
+ { "country": { "name": "Equatorial Guinea", "code": "GQ" } },
+ { "country": { "name": "Eritrea", "code": "ER" } },
+ { "country": { "name": "Estonia", "code": "EE" } },
+ { "country": { "name": "Ethiopia", "code": "ET" } },
+ { "country": { "name": "Falkland Islands (Malvinas", "code": "FK" } },
+ { "country": { "name": "Faroe Islands", "code": "FO" } },
+ { "country": { "name": "Fiji", "code": "FJ" } },
+ { "country": { "name": "Finland", "code": "FI" } },
+ { "country": { "name": "France", "code": "FR" } },
+ { "country": { "name": "French Guiana", "code": "GF" } },
+ { "country": { "name": "French Polynesia", "code": "PF" } },
+ { "country": { "name": "French Southern Territories", "code": "TF" } },
+ { "country": { "name": "Gabon", "code": "GA" } },
+ { "country": { "name": "Gambia", "code": "GM" } },
+ { "country": { "name": "Georgia", "code": "GE" } },
+ { "country": { "name": "Germany", "code": "DE" } },
+ { "country": { "name": "Ghana", "code": "GH" } },
+ { "country": { "name": "Gibraltar", "code": "GI" } },
+ { "country": { "name": "Greece", "code": "GR" } },
+ { "country": { "name": "Greenland", "code": "GL" } },
+ { "country": { "name": "Grenada", "code": "GD" } },
+ { "country": { "name": "Guadeloupe", "code": "GP" } },
+ { "country": { "name": "Guam", "code": "GU" } },
+ { "country": { "name": "Guatemala", "code": "GT" } },
+ { "country": { "name": "Guernsey", "code": "GG" } },
+ { "country": { "name": "Guinea", "code": "GN" } },
+ { "country": { "name": "Guinea-Bissau", "code": "GW" } },
+ { "country": { "name": "Guyana", "code": "GY" } },
+ { "country": { "name": "Haiti", "code": "HT" } },
+ { "country": { "name": "Heard Island and Mcdonald Islands", "code": "HM" } },
+ { "country": { "name": "Holy See (Vatican City State", "code": "VA" } },
+ { "country": { "name": "Honduras", "code": "HN" } },
+ { "country": { "name": "Hong Kong", "code": "HK" } },
+ { "country": { "name": "Hungary", "code": "HU" } },
+ { "country": { "name": "Iceland", "code": "IS" } },
+ { "country": { "name": "India", "code": "IN" } },
+ { "country": { "name": "Indonesia", "code": "ID" } },
+ { "country": { "name": "Iran, Islamic Republic Of", "code": "IR" } },
+ { "country": { "name": "Iraq", "code": "IQ" } },
+ { "country": { "name": "Ireland", "code": "IE" } },
+ { "country": { "name": "Isle of Man", "code": "IM" } },
+ { "country": { "name": "Israel", "code": "IL" } },
+ { "country": { "name": "Italy", "code": "IT" } },
+ { "country": { "name": "Jamaica", "code": "JM" } },
+ { "country": { "name": "Japan", "code": "JP" } },
+ { "country": { "name": "Jersey", "code": "JE" } },
+ { "country": { "name": "Jordan", "code": "JO" } },
+ { "country": { "name": "Kazakhstan", "code": "KZ" } },
+ { "country": { "name": "Kenya", "code": "KE" } },
+ { "country": { "name": "Kiribati", "code": "KI" } },
+ { "country": { "name": "Korea, Democratic People's Republic of", "code": "KP" } },
+ { "country": { "name": "Korea, Republic of", "code": "KR" } },
+ { "country": { "name": "Kuwait", "code": "KW" } },
+ { "country": { "name": "Kyrgyzstan", "code": "KG" } },
+ { "country": { "name": "Lao People's Democratic Republic", "code": "LA" } },
+ { "country": { "name": "Latvia", "code": "LV" } },
+ { "country": { "name": "Lebanon", "code": "LB" } },
+ { "country": { "name": "Lesotho", "code": "LS" } },
+ { "country": { "name": "Liberia", "code": "LR" } },
+ { "country": { "name": "Libyan Arab Jamahiriya", "code": "LY" } },
+ { "country": { "name": "Liechtenstein", "code": "LI" } },
+ { "country": { "name": "Lithuania", "code": "LT" } },
+ { "country": { "name": "Luxembourg", "code": "LU" } },
+ { "country": { "name": "Macao", "code": "MO" } },
+ { "country": { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" } },
+ { "country": { "name": "Madagascar", "code": "MG" } },
+ { "country": { "name": "Malawi", "code": "MW" } },
+ { "country": { "name": "Malaysia", "code": "MY" } },
+ { "country": { "name": "Maldives", "code": "MV" } },
+ { "country": { "name": "Mali", "code": "ML" } },
+ { "country": { "name": "Malta", "code": "MT" } },
+ { "country": { "name": "Marshall Islands", "code": "MH" } },
+ { "country": { "name": "Martinique", "code": "MQ" } },
+ { "country": { "name": "Mauritania", "code": "MR" } },
+ { "country": { "name": "Mauritius", "code": "MU" } },
+ { "country": { "name": "Mayotte", "code": "YT" } },
+ { "country": { "name": "Mexico", "code": "MX" } },
+ { "country": { "name": "Micronesia, Federated States of", "code": "FM" } },
+ { "country": { "name": "Moldova, Republic of", "code": "MD" } },
+ { "country": { "name": "Monaco", "code": "MC" } },
+ { "country": { "name": "Mongolia", "code": "MN" } },
+ { "country": { "name": "Montserrat", "code": "MS" } },
+ { "country": { "name": "Morocco", "code": "MA" } },
+ { "country": { "name": "Mozambique", "code": "MZ" } },
+ { "country": { "name": "Myanmar", "code": "MM" } },
+ { "country": { "name": "Namibia", "code": "NA" } },
+ { "country": { "name": "Nauru", "code": "NR" } },
+ { "country": { "name": "Nepal", "code": "NP" } },
+ { "country": { "name": "Netherlands", "code": "NL" } },
+ { "country": { "name": "Netherlands Antilles", "code": "AN" } },
+ { "country": { "name": "New Caledonia", "code": "NC" } },
+ { "country": { "name": "New Zealand", "code": "NZ" } },
+ { "country": { "name": "Nicaragua", "code": "NI" } },
+ { "country": { "name": "Niger", "code": "NE" } },
+ { "country": { "name": "Nigeria", "code": "NG" } },
+ { "country": { "name": "Niue", "code": "NU" } },
+ { "country": { "name": "Norfolk Island", "code": "NF" } },
+ { "country": { "name": "Northern Mariana Islands", "code": "MP" } },
+ { "country": { "name": "Norway", "code": "NO" } },
+ { "country": { "name": "Oman", "code": "OM" } },
+ { "country": { "name": "Pakistan", "code": "PK" } },
+ { "country": { "name": "Palau", "code": "PW" } },
+ { "country": { "name": "Palestinian Territory, Occupied", "code": "PS" } },
+ { "country": { "name": "Panama", "code": "PA" } },
+ { "country": { "name": "Papua New Guinea", "code": "PG" } },
+ { "country": { "name": "Paraguay", "code": "PY" } },
+ { "country": { "name": "Peru", "code": "PE" } },
+ { "country": { "name": "Philippines", "code": "PH" } },
+ { "country": { "name": "Pitcairn", "code": "PN" } },
+ { "country": { "name": "Poland", "code": "PL" } },
+ { "country": { "name": "Portugal", "code": "PT" } },
+ { "country": { "name": "Puerto Rico", "code": "PR" } },
+ { "country": { "name": "Qatar", "code": "QA" } },
+ { "country": { "name": "Reunion", "code": "RE" } },
+ { "country": { "name": "Romania", "code": "RO" } },
+ { "country": { "name": "Russian Federation", "code": "RU" } },
+ { "country": { "name": "RWANDA", "code": "RW" } },
+ { "country": { "name": "Saint Helena", "code": "SH" } },
+ { "country": { "name": "Saint Kitts and Nevis", "code": "KN" } },
+ { "country": { "name": "Saint Lucia", "code": "LC" } },
+ { "country": { "name": "Saint Pierre and Miquelon", "code": "PM" } },
+ { "country": { "name": "Saint Vincent and the Grenadines", "code": "VC" } },
+ { "country": { "name": "Samoa", "code": "WS" } },
+ { "country": { "name": "San Marino", "code": "SM" } },
+ { "country": { "name": "Sao Tome and Principe", "code": "ST" } },
+ { "country": { "name": "Saudi Arabia", "code": "SA" } },
+ { "country": { "name": "Senegal", "code": "SN" } },
+ { "country": { "name": "Serbia and Montenegro", "code": "CS" } },
+ { "country": { "name": "Seychelles", "code": "SC" } },
+ { "country": { "name": "Sierra Leone", "code": "SL" } },
+ { "country": { "name": "Singapore", "code": "SG" } },
+ { "country": { "name": "Slovakia", "code": "SK" } },
+ { "country": { "name": "Slovenia", "code": "SI" } },
+ { "country": { "name": "Solomon Islands", "code": "SB" } },
+ { "country": { "name": "Somalia", "code": "SO" } },
+ { "country": { "name": "South Africa", "code": "ZA" } },
+ { "country": { "name": "South Georgia and the South Sandwich Islands", "code": "GS" } },
+ { "country": { "name": "Spain", "code": "ES" } },
+ { "country": { "name": "Sri Lanka", "code": "LK" } },
+ { "country": { "name": "Sudan", "code": "SD" } },
+ { "country": { "name": "Suriname", "code": "SR" } },
+ { "country": { "name": "Svalbard and Jan Mayen", "code": "SJ" } },
+ { "country": { "name": "Swaziland", "code": "SZ" } },
+ { "country": { "name": "Sweden", "code": "SE" } },
+ { "country": { "name": "Switzerland", "code": "CH" } },
+ { "country": { "name": "Syrian Arab Republic", "code": "SY" } },
+ { "country": { "name": "Taiwan, Province of China", "code": "TW" } },
+ { "country": { "name": "Tajikistan", "code": "TJ" } },
+ { "country": { "name": "Tanzania, United Republic of", "code": "TZ" } },
+ { "country": { "name": "Thailand", "code": "TH" } },
+ { "country": { "name": "Timor-Leste", "code": "TL" } },
+ { "country": { "name": "Togo", "code": "TG" } },
+ { "country": { "name": "Tokelau", "code": "TK" } },
+ { "country": { "name": "Tonga", "code": "TO" } },
+ { "country": { "name": "Trinidad and Tobago", "code": "TT" } },
+ { "country": { "name": "Tunisia", "code": "TN" } },
+ { "country": { "name": "Turkey", "code": "TR" } },
+ { "country": { "name": "Turkmenistan", "code": "TM" } },
+ { "country": { "name": "Turks and Caicos Islands", "code": "TC" } },
+ { "country": { "name": "Tuvalu", "code": "TV" } },
+ { "country": { "name": "Uganda", "code": "UG" } },
+ { "country": { "name": "Ukraine", "code": "UA" } },
+ { "country": { "name": "United Arab Emirates", "code": "AE" } },
+ { "country": { "name": "United Kingdom", "code": "GB" } },
+ { "country": { "name": "United States", "code": "US" } },
+ { "country": { "name": "United States Minor Outlying Islands", "code": "UM" } },
+ { "country": { "name": "Uruguay", "code": "UY" } },
+ { "country": { "name": "Uzbekistan", "code": "UZ" } },
+ { "country": { "name": "Vanuatu", "code": "VU" } },
+ { "country": { "name": "Venezuela", "code": "VE" } },
+ { "country": { "name": "Viet Nam", "code": "VN" } },
+ { "country": { "name": "Virgin Islands, British", "code": "VG" } },
+ { "country": { "name": "Virgin Islands, U.S", "code": "VI" } },
+ { "country": { "name": "Wallis and Futuna", "code": "WF" } },
+ { "country": { "name": "Western Sahara", "code": "EH" } },
+ { "country": { "name": "Yemen", "code": "YE" } },
+ { "country": { "name": "Zambia", "code": "ZM" } },
+ { "country": { "name": "Zimbabwe", "code": "ZW" } }
+ ]
+}
diff --git a/packages/oui-autocomplete/src/index.spec.js b/packages/oui-autocomplete/src/index.spec.js
new file mode 100644
index 00000000..c0e337ae
--- /dev/null
+++ b/packages/oui-autocomplete/src/index.spec.js
@@ -0,0 +1,197 @@
+import data from "./index.spec.data.json";
+
+describe("ouiAutocomplete", () => {
+ let $document;
+ let $timeout;
+ let testUtils;
+
+ beforeEach(angular.mock.module("oui.autocomplete"));
+ beforeEach(angular.mock.module("oui.autocomplete.configuration"));
+ beforeEach(angular.mock.module("oui.test-utils"));
+
+ beforeEach(inject((_$document_, _$timeout_, _TestUtils_) => {
+ $document = _$document_;
+ $timeout = _$timeout_;
+ testUtils = _TestUtils_;
+ }));
+
+ describe("Provider", () => {
+ let configuration;
+
+ angular.module("oui.autocomplete.configuration", [
+ "oui.autocomplete"
+ ]).config(ouiAutocompleteConfigurationProvider => {
+ ouiAutocompleteConfigurationProvider.setOptions({
+ foo: "bar"
+ });
+ });
+
+ beforeEach(inject(_ouiAutocompleteConfiguration_ => {
+ configuration = _ouiAutocompleteConfiguration_;
+ }));
+
+ it("should have custom options", () => {
+ expect(configuration.options.foo).toEqual("bar");
+ });
+ });
+
+ describe("Directive", () => {
+ describe("Init", () => {
+ let component;
+ let input;
+ let autocomplete;
+ let autocompleteCtrl;
+ let controller;
+
+ beforeEach(() => {
+ component = testUtils.compileTemplate(``, {
+ suggestions: data.strings
+ });
+
+ $timeout.flush();
+
+ input = angular.element(component[0].querySelector(".trigger"));
+ autocomplete = input.next();
+ autocompleteCtrl = component.scope().$$childHead.$autocompleteCtrl;
+ controller = component.scope().$ctrl;
+ });
+
+ it("should prepare trigger input and create a datalist, next to it", () => {
+ expect(input.attr("list")).toBeDefined();
+ expect(input.attr("autocomplete")).toBe("off");
+
+ expect(autocomplete.length).toBe(1);
+ expect(autocomplete.attr("id")).toBeDefined();
+ expect(autocomplete.hasClass("oui-autocomplete")).toBeTruthy();
+ });
+
+ it("should create popper helper when input is focused", () => {
+ expect(autocompleteCtrl.popper).toBeUndefined();
+
+ input.triggerHandler("focus");
+ expect(autocompleteCtrl.popper).toBeDefined();
+ });
+
+ it("should initialize search engine", () => {
+ const engine = autocompleteCtrl.engine;
+ expect(engine.local.length).toBe(controller.suggestions.length);
+ });
+
+ it("should update search engine", () => {
+ controller.suggestions = [];
+ component.scope().$apply();
+
+ const engine = autocompleteCtrl.engine;
+ expect(engine.local.length).toBe(controller.suggestions.length);
+ });
+ });
+
+ describe("Search", () => {
+ const debounceDelay = 500;
+ const searchValue = "belgium";
+ const KEYBOARD_KEYS = {
+ TAB: 9,
+ SHIFT: 16,
+ ESC: 27,
+ UP: 38,
+ DOWN: 40
+ };
+
+ let component;
+ let input;
+ let autocomplete;
+ let autocompleteCtrl;
+
+ beforeEach((done) => {
+ component = testUtils.compileTemplate(``, {
+ suggestions: data.strings
+ });
+
+ $timeout.flush();
+
+ input = angular.element(component[0].querySelector(".trigger"));
+ autocomplete = input.next();
+ autocompleteCtrl = component.scope().$$childHead.$autocompleteCtrl;
+
+ // Update input value and notify change to angularjs
+ input.val(searchValue);
+ input.triggerHandler("input");
+
+ // Autocomplete has debounce delay before showing up
+ setTimeout(() => {
+ $timeout.flush();
+ done();
+ }, debounceDelay);
+ });
+
+ it("should update input value", () => {
+ const suggestion = autocomplete.find("button");
+
+ expect(autocomplete.find("button").length).toBe(1);
+ expect(suggestion.text().toLowerCase()).toBe(searchValue);
+
+ suggestion.triggerHandler("click");
+ expect(input.val()).toBe(suggestion.text());
+ });
+
+ it("should navigate through suggestions with keyboard", () => {
+ // Navigation start at last index of array (trigger input)
+ // And is initialized on first keydown
+ expect(autocompleteCtrl.navIndex).toBeUndefined();
+
+ // Should be first item (loop)
+ $document.triggerHandler({ type: "keydown", which: KEYBOARD_KEYS.DOWN });
+ $document.triggerHandler({ type: "keyup", which: KEYBOARD_KEYS.DOWN });
+ expect(autocompleteCtrl.navIndex).toBe(0);
+
+ // Should be next item
+ $document.triggerHandler({ type: "keydown", which: KEYBOARD_KEYS.TAB });
+ $document.triggerHandler({ type: "keyup", which: KEYBOARD_KEYS.TAB });
+ expect(autocompleteCtrl.navIndex).toBe(autocompleteCtrl.navLastIndex);
+
+ // Should be prev item
+ $document.triggerHandler({ type: "keydown", which: KEYBOARD_KEYS.UP });
+ $document.triggerHandler({ type: "keyup", which: KEYBOARD_KEYS.UP });
+ expect(autocompleteCtrl.navIndex).toBe(0);
+
+ // Should be last item (loop)
+ $document.triggerHandler({ type: "keydown", which: KEYBOARD_KEYS.SHIFT });
+ $document.triggerHandler({ type: "keydown", which: KEYBOARD_KEYS.TAB });
+ $document.triggerHandler({ type: "keyup", which: KEYBOARD_KEYS.SHIFT });
+ $document.triggerHandler({ type: "keyup", which: KEYBOARD_KEYS.TAB });
+ expect(autocompleteCtrl.navIndex).toBe(autocompleteCtrl.navLastIndex);
+
+ // Should close datalist
+ $document.triggerHandler({ type: "keydown", which: KEYBOARD_KEYS.ESC });
+ $document.triggerHandler({ type: "keyup", which: KEYBOARD_KEYS.ESC });
+
+ $timeout.flush();
+
+ expect(autocomplete.hasClass("ng-hide")).toBeTruthy();
+ });
+
+ it("should close datalist when below minimum length", (done) => {
+ input.val("");
+ input.triggerHandler("input");
+
+ setTimeout(() => {
+ $timeout.flush();
+ expect(autocomplete.hasClass("ng-hide")).toBeTruthy();
+ done();
+ }, debounceDelay);
+ });
+
+ it("should close datalist when clicked outside", () => {
+ $document.triggerHandler("click");
+
+ $timeout.flush();
+
+ expect(autocomplete.hasClass("ng-hide")).toBeTruthy();
+ });
+ });
+ });
+});
diff --git a/packages/oui-button/src/button.html b/packages/oui-button/src/button.html
index 1106a01d..ef95d5c2 100644
--- a/packages/oui-button/src/button.html
+++ b/packages/oui-button/src/button.html
@@ -16,7 +16,7 @@
>
- {{::$ctrl.text}}
+ {{$ctrl.text}}
diff --git a/packages/oui-checkbox/src/checkbox.component.js b/packages/oui-checkbox/src/checkbox.component.js
index 3663b59a..9e789c78 100644
--- a/packages/oui-checkbox/src/checkbox.component.js
+++ b/packages/oui-checkbox/src/checkbox.component.js
@@ -4,6 +4,9 @@ import template from "./checkbox.html";
export default {
template,
controller,
+ require: {
+ form: "?^^form"
+ },
bindings: {
model: "=?",
id: "@?",
diff --git a/packages/oui-checkbox/src/checkbox.controller.js b/packages/oui-checkbox/src/checkbox.controller.js
index bdf5101e..053b27da 100644
--- a/packages/oui-checkbox/src/checkbox.controller.js
+++ b/packages/oui-checkbox/src/checkbox.controller.js
@@ -17,7 +17,6 @@ export default class {
// So we use $timeout to force the $apply
this.$timeout(() =>
this.$element
- .addClass("oui-checkbox")
.removeAttr("id")
.removeAttr("name")
);
@@ -38,6 +37,13 @@ export default class {
addDefaultParameter(this, "id", `ouiCheckbox${this.$scope.$id}`);
}
+ hasError () {
+ if (!this.form || !this.form[this.name]) {
+ return false;
+ }
+ return (this.form[this.name].$dirty || this.form.$submitted) && !this.focused && this.form[this.name].$invalid;
+ }
+
_updateIndeterminateState (model) {
this.checkboxElement.prop("indeterminate", model === null);
}
diff --git a/packages/oui-checkbox/src/checkbox.html b/packages/oui-checkbox/src/checkbox.html
index fc1249ef..4caca23f 100644
--- a/packages/oui-checkbox/src/checkbox.html
+++ b/packages/oui-checkbox/src/checkbox.html
@@ -1,28 +1,33 @@
-
-
diff --git a/packages/oui-select/README.md b/packages/oui-select/README.md
index d6b4e6a7..766721fa 100644
--- a/packages/oui-select/README.md
+++ b/packages/oui-select/README.md
@@ -4,51 +4,44 @@
## Usage
+### Basic (String array)
+
+```html:preview
+
+
+```
+
### Basic (Object array)
```html:preview
-
+ match="name">
```
-### Basic (String array)
+### Placeholder
```html:preview
-
+ items="['a', 'b', 'c']">
```
-### Grouping / Custom template
+### Searchable
```html:preview
-
-
- Code:
-
+ searchable>
```
@@ -56,44 +49,76 @@
```html:preview
-
```
### Disabled Items
+
+ For each $item in items array, disable-item will be called with current $item as an argument.
+ If it returns true, $item will be disabled.
+
+
```html:preview
-
+ match="name">
```
-**Note**: For each `$item` in `items` array, `disable-item` will be called with current `$item` as an argument. If it returns true, `$item` will be disabled.
+### Grouping
+
+```html:preview
+
+
+```
+
+### Custom option template
+
+
+ Template inside oui-select component will be used as the content of each option.
+ You can use $item variable to get option value for your template.
+
+
+```html:preview
+
+
+
+ Code:
+
+
+```
### On Change
-**Note**: Model will not be refreshed until the `on-change` callback hasn't returned. If you want to access the new model inside the `on-change` callback you need to use the `modelValue` variable as below.
+
+ Model will not be refreshed until the on-change callback hasn't returned.
+ If you want to access the new model inside the on-change callback you need to use the modelValue variable as below.
+
```html:preview
-
+ on-focus="$ctrl.onFocus()">
+
Code:
@@ -123,8 +147,7 @@
| ---- | ---- | ---- | ---- | ---- | ---- | ----
| `model` | object | = | no | n/a | n/a | model bound to component
| `name` | string | @? | yes | n/a | n/a | name of the form component
-| `data-align` | string | @? | yes | `start`, `end` | `start` | dropdown alignment
-| `data-title` | string | @? | yes | n/a | n/a | title attribute of the component
+| `title` | string | @? | yes | n/a | n/a | title attribute of the component
| `placeholder` | string | @? | yes | n/a | n/a | placeholder displayed when model is undefined
| `match` | string | @? | no | n/a | n/a | property of item to show as selected item
| `items` | array | < | no | n/a | n/a | array used to populate the list
@@ -136,3 +159,6 @@
| `on-focus` | function | & | no | n/a | n/a | called on focus
| `on-change` | function | & | no | n/a | n/a | handler triggered when value has changed
+#### Deprecated
+
+* `data-align`: Unused
diff --git a/packages/oui-select/src/index.js b/packages/oui-select/src/index.js
index 852aa0c3..baf7f18e 100644
--- a/packages/oui-select/src/index.js
+++ b/packages/oui-select/src/index.js
@@ -1,11 +1,21 @@
-import "./ui-select";
+import "ui-select";
import Select from "./select.directive";
export default angular
.module("oui.select", [
"oui.field",
- "oui.ui-select",
+ "ui.select",
"ngSanitize"
])
+ .run(["$templateCache", ($templateCache) => {
+ $templateCache.put("oui-ui-select/choices.tpl.html", require("./templates/choices.html"));
+ $templateCache.put("oui-ui-select/footer.tpl.html", require("./templates/footer.html"));
+ $templateCache.put("oui-ui-select/header.tpl.html", require("./templates/header.html"));
+ $templateCache.put("oui-ui-select/match.tpl.html", require("./templates/match.html"));
+ $templateCache.put("oui-ui-select/match-multiple.tpl.html", require("./templates/match-multiple.html"));
+ $templateCache.put("oui-ui-select/no-choice.tpl.html", require("./templates/no-choice.html"));
+ $templateCache.put("oui-ui-select/select.tpl.html", require("./templates/select.html"));
+ $templateCache.put("oui-ui-select/select-multiple.tpl.html", require("./templates/select-multiple.html"));
+ }])
.directive("ouiSelect", Select)
.name;
diff --git a/packages/oui-select/src/index.spec.js b/packages/oui-select/src/index.spec.js
index 8acbe5cf..604f4ed2 100644
--- a/packages/oui-select/src/index.spec.js
+++ b/packages/oui-select/src/index.spec.js
@@ -15,12 +15,12 @@ describe("ouiSelect", () => {
$timeout = _$timeout_;
}));
- const openClass = "oui-ui-select-container_open";
- const selectedItemClass = "selected";
+ const selectedItemClass = "ui-select-choices-row_selected";
- const getContainer = element => element[0].querySelector(".oui-ui-select-container");
- const getDropdownButton = element => element[0].querySelector(".oui-button_dropdown");
+ const getContainer = element => element[0].querySelector(".ui-select-container");
+ const getDropdownButton = element => element[0].querySelector(".ui-select-match");
const getDropdown = element => element[0].querySelector(".ui-select-choices-content");
+ const getFocusser = element => element[0].querySelector(".ui-select-focusser");
const getItemsGroups = element => element[0].querySelectorAll(".ui-select-choices-group");
const getItemsGroup = (element, index) => element[0].querySelectorAll(".ui-select-choices-group")[index];
const getItemsGroupLabel = groupElement => groupElement.querySelector(".ui-select-choices-group-label");
@@ -35,11 +35,10 @@ describe("ouiSelect", () => {
const element = TestUtils.compileTemplate(`
+ match="name">
`, {
countries: data
@@ -55,28 +54,26 @@ describe("ouiSelect", () => {
const element = TestUtils.compileTemplate(`
+ match="name">
`, {
countries: data
});
- const $container = angular.element(getContainer(element));
const $triggerButton = angular.element(getDropdownButton(element));
// The dropdown should be initially closed.
- expect($container.hasClass(openClass)).toBeFalsy();
+ expect($triggerButton.attr("aria-expanded")).toBe("false");
// Click on the trigger and check if it's open.
$triggerButton.triggerHandler("click");
- expect($container.hasClass(openClass)).toBeTruthy();
+ expect($triggerButton.attr("aria-expanded")).toBe("true");
$triggerButton.triggerHandler("click");
- expect($container.hasClass(openClass)).toBeFalsy();
+ expect($triggerButton.attr("aria-expanded")).toBe("false");
});
it("should close the dropdown on click outside it", () => {
@@ -84,11 +81,10 @@ describe("ouiSelect", () => {
+ match="name">
@@ -96,7 +92,6 @@ describe("ouiSelect", () => {
countries: data
});
- const $container = angular.element(getContainer(element));
const $triggerButton = angular.element(getDropdownButton(element));
const outsideElement = element[0].querySelector(".outside-button");
@@ -105,28 +100,26 @@ describe("ouiSelect", () => {
// Close the dropdown by clicking outside the dropdown.
$document.triggerHandler({ type: "click", target: outsideElement });
- expect($container.hasClass(openClass)).toBeFalsy();
+ expect($triggerButton.attr("aria-expanded")).toBe("false");
});
describe("Single select", () => {
- it("should open dropdown when trigger button is clicked", () => {
+ it("should close dropdown when item is select", () => {
const element = TestUtils.compileTemplate(`
+ match="name">
`, {
countries: data
});
- const $container = angular.element(getContainer(element));
const $triggerButton = angular.element(getDropdownButton(element));
- expect($container.hasClass(openClass)).toBeFalsy();
+ expect($triggerButton.attr("aria-expanded")).toBe("false");
// Open the dropdown
$triggerButton.triggerHandler("click");
@@ -135,13 +128,13 @@ describe("ouiSelect", () => {
let $itemButton = angular.element(getDropdownItem(element, 4)); // eslint-disable-line no-magic-numbers
expect($itemButton.hasClass(selectedItemClass)).toBeFalsy();
$itemButton.triggerHandler("click");
- expect($itemButton.hasClass(selectedItemClass)).toBeTruthy();
- // By the way, the dropdown should have been closed.
- expect($container.hasClass(openClass)).toBeFalsy();
+ // The dropdown should have been closed.
+ expect($triggerButton.attr("aria-expanded")).toBe("false");
// Reopen dropdown and check if the selected element is highlighted.
// The element is retrieved again to be sure to not test on a detached element.
+ $triggerButton.triggerHandler("click");
$itemButton = angular.element(getDropdownItem(element, 4)); // eslint-disable-line no-magic-numbers
expect($itemButton.hasClass(selectedItemClass)).toBeTruthy();
});
@@ -152,16 +145,19 @@ describe("ouiSelect", () => {
const element = TestUtils.compileTemplate(`
+ match="name">
`, {
countries: data
});
+ // Must open the select first, to init the dropdown menu
+ const $triggerButton = angular.element(getDropdownButton(element));
+ $triggerButton.triggerHandler("click");
+
expect(getDropdownItems(element).length).toEqual(data.length);
expect(angular.element(getDropdownItem(element, 0)).text()).toContain(data[0].name);
expect(angular.element(getDropdownItem(element, data.length - 1)).text()).toContain(data[data.length - 1].name);
@@ -172,15 +168,18 @@ describe("ouiSelect", () => {
const stringArray = ["a", "b", "c"];
const element = TestUtils.compileTemplate(`
+ items="$ctrl.array">
`, {
array: stringArray
});
+ // Must open the select first, to init the dropdown menu
+ const $triggerButton = angular.element(getDropdownButton(element));
+ $triggerButton.triggerHandler("click");
+
expect(getDropdownItems(element).length).toEqual(stringArray.length);
expect(angular.element(getDropdownItem(element, 0)).text()).toContain(stringArray[0]);
expect(angular.element(getDropdownItem(element, stringArray.length - 1)).text()).toContain(stringArray[stringArray.length - 1]);
@@ -194,18 +193,21 @@ describe("ouiSelect", () => {
const element = TestUtils.compileTemplate(`
+ group-by="$ctrl.groupByFirstLetter">
`, {
countries: data,
groupByFirstLetter
});
+ // Must open the select first, to init the dropdown menu
+ const $triggerButton = angular.element(getDropdownButton(element));
+ $triggerButton.triggerHandler("click");
+
const groups = uniq(data.map(groupByFirstLetter));
const firstGroupElement = getItemsGroup(element, 0);
const lastGroupElement = getItemsGroup(element, groups.length - 1);
@@ -223,7 +225,7 @@ describe("ouiSelect", () => {
const element = TestUtils.compileTemplate(`
{
onBlur
});
- angular.element(getDropdownButton(element)).triggerHandler("blur");
+ $timeout.flush();
+
+ angular.element(getFocusser(element)).triggerHandler("blur");
expect(onBlur).toHaveBeenCalled();
});
});
@@ -244,7 +248,7 @@ describe("ouiSelect", () => {
const element = TestUtils.compileTemplate(`
{
onFocus
});
- angular.element(getDropdownButton(element)).triggerHandler("focus");
+ $timeout.flush();
+
+ angular.element(getFocusser(element)).triggerHandler("focus");
expect(onFocus).toHaveBeenCalled();
});
});
@@ -265,7 +271,7 @@ describe("ouiSelect", () => {
const element = TestUtils.compileTemplate(`
{
onChange
});
- const index1 = 4;
- const index2 = 10;
- let $itemButton = angular.element(getDropdownItem(element, index1));
- $itemButton.triggerHandler("click");
$timeout.flush();
- expect(onChange).toHaveBeenCalledWith(data[index1]);
- $itemButton = angular.element(getDropdownItem(element, index2));
+ // Must open the select first, to init the dropdown menu
+ const $triggerButton = angular.element(getDropdownButton(element));
+ $triggerButton.triggerHandler("click");
+
+ const index = 4;
+ const $itemButton = angular.element(getDropdownItem(element, index));
$itemButton.triggerHandler("click");
+
+ // onSelect from ui-select is inside a $timeout
+ // Must open the dropdown before flushing the $timeout
+ $triggerButton.triggerHandler("click");
$timeout.flush();
- expect(onChange).toHaveBeenCalledWith(data[index2]);
+ expect(onChange).toHaveBeenCalledWith(data[index]);
});
});
diff --git a/packages/oui-select/src/select.controller.js b/packages/oui-select/src/select.controller.js
index 46e10231..76fcc69a 100644
--- a/packages/oui-select/src/select.controller.js
+++ b/packages/oui-select/src/select.controller.js
@@ -1,8 +1,5 @@
import { addBooleanParameter } from "@ovh-ui/common/component-utils";
-const UI_SELECT_SELECTOR = ".oui-ui-select-container";
-const UI_SELECT_DROPDOWN_TRIGGER = ".oui-button_dropdown";
-
export default class {
constructor ($attrs, $compile, $element, $scope, $timeout) {
"ngInject";
@@ -17,32 +14,26 @@ export default class {
$onInit () {
addBooleanParameter(this, "disabled");
addBooleanParameter(this, "required");
+ addBooleanParameter(this, "searchable");
}
$postLink () {
const $htmlContent = angular.element(this.htmlContent);
- const matchElement = $htmlContent.find("oui-ui-select-match");
-
- if (this.match) {
- matchElement.html(`{{$select.selected.${this.match}}}`);
- } else {
- matchElement.html("{{$select.selected}}");
- }
-
this.$compile($htmlContent)(this.$scope, (clone) => {
this.$element.append(clone);
});
this.$timeout(() => {
- this.$element.removeAttr("name");
-
- this.uiSelectElement = this.$element[0].querySelector(UI_SELECT_SELECTOR);
- this.uiSelectDropdownTrigger = this.$element[0].querySelector(UI_SELECT_DROPDOWN_TRIGGER);
+ this.$element
+ .removeAttr("name")
+ .removeAttr("title");
- this.unregisterFocus = this.$scope.$on("oui:focus", () => {
- this.uiSelectDropdownTrigger.focus();
- });
+ this.$select.focusser
+ .on("blur", () => this.onUiSelectBlur())
+ .on("focus", () => this.onUiSelectFocus());
});
+
+ this.unregisterFocus = this.$scope.$on("oui:focus", () => this.$select.setFocus());
}
$destroy () {
@@ -54,7 +45,7 @@ export default class {
onUiSelectBlur () {
if (this.fieldCtrl) {
this.fieldCtrl.hasFocus = false;
- this.fieldCtrl.checkControlErrors(this.uiSelectElement, this.name);
+ this.fieldCtrl.checkControlErrors(this.$select.$element[0], this.name);
}
this.onBlur();
@@ -63,7 +54,7 @@ export default class {
onUiSelectFocus () {
if (this.fieldCtrl) {
this.fieldCtrl.hasFocus = true;
- this.fieldCtrl.hideErrors(this.uiSelectElement, this.name);
+ this.fieldCtrl.hideErrors(this.$select.$element[0], this.name);
}
this.onFocus();
diff --git a/packages/oui-select/src/select.directive.js b/packages/oui-select/src/select.directive.js
index 45a76cd9..20aa8d0e 100644
--- a/packages/oui-select/src/select.directive.js
+++ b/packages/oui-select/src/select.directive.js
@@ -12,25 +12,27 @@ export default () => ({
scope: {
model: "=",
name: "@?",
- required: "",
- disabled: "",
- title: "@?",
placeholder: "@?",
+ title: "@?",
items: "<",
disableItems: "&",
match: "@?",
groupBy: "",
- align: "@?",
+ required: "",
+ disabled: "",
+ searchable: "",
onBlur: "&",
onFocus: "&",
onChange: "&"
},
compile: ($element, $attrs) => {
- const itemTemplate = $element.html();
+ const itemTemplate = $element.html().trim();
const $template = angular.element(template);
- const choicesElement = $template.find("oui-ui-select-choices");
+ const choicesElement = $template.find("ui-select-choices");
- choicesElement.html(itemTemplate);
+ if (itemTemplate) {
+ choicesElement.html(itemTemplate);
+ }
if ($attrs.groupBy) {
choicesElement.attr("group-by", "$ctrl.groupBy");
}
diff --git a/packages/oui-select/src/select.html b/packages/oui-select/src/select.html
index 3fc01767..6b388bf4 100644
--- a/packages/oui-select/src/select.html
+++ b/packages/oui-select/src/select.html
@@ -1,13 +1,16 @@
-
-
-
-
+ on-select="$ctrl.onChange({ modelValue: $model })"
+ search-enabled="{{::!!$ctrl.searchable}}"
+ theme="oui-ui-select">
+
+
+
+
+
diff --git a/packages/oui-select/src/templates/choices.html b/packages/oui-select/src/templates/choices.html
index 80888d58..768e6c5a 100644
--- a/packages/oui-select/src/templates/choices.html
+++ b/packages/oui-select/src/templates/choices.html
@@ -1,19 +1,22 @@
- -
-
-
diff --git a/packages/oui-select/src/templates/footer.html b/packages/oui-select/src/templates/footer.html
new file mode 100644
index 00000000..2eb79699
--- /dev/null
+++ b/packages/oui-select/src/templates/footer.html
@@ -0,0 +1 @@
+
diff --git a/packages/oui-select/src/templates/header.html b/packages/oui-select/src/templates/header.html
new file mode 100644
index 00000000..4883b633
--- /dev/null
+++ b/packages/oui-select/src/templates/header.html
@@ -0,0 +1 @@
+
diff --git a/packages/oui-select/src/templates/match-multiple.html b/packages/oui-select/src/templates/match-multiple.html
new file mode 100644
index 00000000..04def96c
--- /dev/null
+++ b/packages/oui-select/src/templates/match-multiple.html
@@ -0,0 +1,16 @@
+
+
+
+ ×
+
+
+
+
diff --git a/packages/oui-select/src/templates/match.html b/packages/oui-select/src/templates/match.html
index d73c63b0..4cff7035 100644
--- a/packages/oui-select/src/templates/match.html
+++ b/packages/oui-select/src/templates/match.html
@@ -1,15 +1,22 @@
-
- {{$select.placeholder}}
-
-
-
-
-
+ tabindex="-1"
+ type="button">
+
+
+
+
+
+
+
diff --git a/packages/oui-select/src/templates/no-choice.html b/packages/oui-select/src/templates/no-choice.html
new file mode 100644
index 00000000..ae611c03
--- /dev/null
+++ b/packages/oui-select/src/templates/no-choice.html
@@ -0,0 +1,4 @@
+
diff --git a/packages/oui-select/src/templates/select-multiple.html b/packages/oui-select/src/templates/select-multiple.html
new file mode 100644
index 00000000..bc470935
--- /dev/null
+++ b/packages/oui-select/src/templates/select-multiple.html
@@ -0,0 +1,24 @@
+
diff --git a/packages/oui-select/src/templates/select.html b/packages/oui-select/src/templates/select.html
index 0c2797cc..eef5eea9 100644
--- a/packages/oui-select/src/templates/select.html
+++ b/packages/oui-select/src/templates/select.html
@@ -1,9 +1,22 @@
-
-
-
-
-
-
+
diff --git a/packages/oui-select/src/ui-select.js b/packages/oui-select/src/ui-select.js
deleted file mode 100644
index ba7a94b9..00000000
--- a/packages/oui-select/src/ui-select.js
+++ /dev/null
@@ -1,2479 +0,0 @@
-import Popper from "popper.js";
-
-/* eslint-disable */
-
-/*!
- * ui-select
- * http://github.com/angular-ui/ui-select
- * Version: 0.19.7 - 2017-04-15T14:28:36.649Z
- * License: MIT
- */
-
-
-(function () {
-"use strict";
-var KEY = {
- TAB: 9,
- ENTER: 13,
- ESC: 27,
- SPACE: 32,
- LEFT: 37,
- UP: 38,
- RIGHT: 39,
- DOWN: 40,
- SHIFT: 16,
- CTRL: 17,
- ALT: 18,
- PAGE_UP: 33,
- PAGE_DOWN: 34,
- HOME: 36,
- END: 35,
- BACKSPACE: 8,
- DELETE: 46,
- COMMAND: 91,
-
- MAP: { 91 : "COMMAND", 8 : "BACKSPACE" , 9 : "TAB" , 13 : "ENTER" , 16 : "SHIFT" , 17 : "CTRL" , 18 : "ALT" , 19 : "PAUSEBREAK" , 20 : "CAPSLOCK" , 27 : "ESC" , 32 : "SPACE" , 33 : "PAGE_UP", 34 : "PAGE_DOWN" , 35 : "END" , 36 : "HOME" , 37 : "LEFT" , 38 : "UP" , 39 : "RIGHT" , 40 : "DOWN" , 43 : "+" , 44 : "PRINTSCREEN" , 45 : "INSERT" , 46 : "DELETE", 48 : "0" , 49 : "1" , 50 : "2" , 51 : "3" , 52 : "4" , 53 : "5" , 54 : "6" , 55 : "7" , 56 : "8" , 57 : "9" , 59 : ";", 61 : "=" , 65 : "A" , 66 : "B" , 67 : "C" , 68 : "D" , 69 : "E" , 70 : "F" , 71 : "G" , 72 : "H" , 73 : "I" , 74 : "J" , 75 : "K" , 76 : "L", 77 : "M" , 78 : "N" , 79 : "O" , 80 : "P" , 81 : "Q" , 82 : "R" , 83 : "S" , 84 : "T" , 85 : "U" , 86 : "V" , 87 : "W" , 88 : "X" , 89 : "Y" , 90 : "Z", 96 : "0" , 97 : "1" , 98 : "2" , 99 : "3" , 100 : "4" , 101 : "5" , 102 : "6" , 103 : "7" , 104 : "8" , 105 : "9", 106 : "*" , 107 : "+" , 109 : "-" , 110 : "." , 111 : "/", 112 : "F1" , 113 : "F2" , 114 : "F3" , 115 : "F4" , 116 : "F5" , 117 : "F6" , 118 : "F7" , 119 : "F8" , 120 : "F9" , 121 : "F10" , 122 : "F11" , 123 : "F12", 144 : "NUMLOCK" , 145 : "SCROLLLOCK" , 186 : ";" , 187 : "=" , 188 : "," , 189 : "-" , 190 : "." , 191 : "/" , 192 : "`" , 219 : "[" , 220 : "\\" , 221 : "]" , 222 : "'"
- },
-
- isControl: function (e) {
- var k = e.which;
- switch (k) {
- case KEY.COMMAND:
- case KEY.SHIFT:
- case KEY.CTRL:
- case KEY.ALT:
- return true;
- }
-
- if (e.metaKey || e.ctrlKey || e.altKey) return true;
-
- return false;
- },
- isFunctionKey: function (k) {
- k = k.which ? k.which : k;
- return k >= 112 && k <= 123;
- },
- isVerticalMovement: function (k){
- return ~[KEY.UP, KEY.DOWN].indexOf(k);
- },
- isHorizontalMovement: function (k){
- return ~[KEY.LEFT,KEY.RIGHT,KEY.BACKSPACE,KEY.DELETE].indexOf(k);
- },
- toSeparator: function (k) {
- var sep = {ENTER:"\n",TAB:"\t",SPACE:" "}[k];
- if (sep) return sep;
- // return undefined for special keys other than enter, tab or space.
- // no way to use them to cut strings.
- return KEY[k] ? undefined : k;
- }
- };
-
-function isNil(value) {
- return angular.isUndefined(value) || value === null;
-}
-
-/**
- * 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));
- };
-}
-
-/**
- * Add closest() to jqLite.
- */
-if (angular.element.prototype.closest === undefined) {
- angular.element.prototype.closest = function( selector) {
- var elem = this[0];
- var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
-
- while (elem) {
- if (matchesSelector.bind(elem)(selector)) {
- return elem;
- } else {
- elem = elem.parentElement;
- }
- }
- return false;
- };
-}
-
-var latestId = 0;
-
-var uis = angular.module('oui.ui-select', [])
-
-.constant('ouiUiSelectConfig', {
- theme: 'oui.ui-select',
- searchEnabled: true,
- sortable: false,
- placeholder: '', // Empty by default, like HTML tag