diff --git a/HISTORY.md b/HISTORY.md
index f923297..ac56973 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,6 +1,12 @@
MassAutocomplete
================
+## v0.6.0
+- Set `position:absolute` in directive, so it won't trigger a CSP error (PR #67, Thanks @thany!)
+
+## v0.5.0
+- Fix debounce function to use $timeout instead of setTimeout (PR #61, Thanks @OoDeLally!)
+
### v0.4.0
- Lint & style changes.
- (Breaking change) Renamed MassautoCompleteConfigurrerProvider -> massAutocompleteConfig to match directive name.
diff --git a/bower.json b/bower.json
index 1e5b65d..3b9d468 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
{
"name": "angular-mass-autocomplete",
- "version": "0.4.0",
+ "version": "0.6.0",
"main": "massautocomplete.min.js",
"dependencies": {
"angular": ">=1.2.0"
diff --git a/massautocomplete.js b/massautocomplete.js
index 4ea2627..6fb77c2 100644
--- a/massautocomplete.js
+++ b/massautocomplete.js
@@ -1,461 +1,503 @@
/* global angular */
-(function() {
-'use strict';
-
-angular.module('MassAutoComplete', [])
-
-.provider('massAutocompleteConfig', function() {
- var config = this;
-
- config.KEYS = {
- TAB: 9,
- ESC: 27,
- ENTER: 13,
- UP: 38,
- DOWN: 40
- };
-
- config.EVENTS = {
- KEYDOWN: 'keydown',
- RESIZE: 'resize',
- BLUR: 'blur'
- };
-
- config.DEBOUNCE = {
- position: 150,
- attach: 300,
- suggest: 200,
- blur: 150
- };
-
- config.generate_random_id = function(prefix) {
- return prefix + '_' + Math.random().toString().substring(2);
- };
-
- // Position ac container given a target element
- config.position_autocomplete = function(container, target) {
- var rect = target[0].getBoundingClientRect(),
- scrollTop = document.body.scrollTop || document.documentElement.scrollTop || window.pageYOffset,
- scrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft || window.pageXOffset;
-
- container[0].style.top = rect.top + rect.height + scrollTop + 'px';
- container[0].style.left = rect.left + scrollLeft + 'px';
- container[0].style.width = rect.width + 'px';
- };
-
- this.$get = function() {
- return config;
- };
-})
-
-.directive('massAutocomplete', ['massAutocompleteConfig', '$timeout', '$window', '$document', '$q', function(config, $timeout, $window, $document, $q) {
- return {
- restrict: 'A',
- scope: {
- options: '&massAutocomplete'
- },
- transclude: true,
- template:
- '' +
-
- '
' +
-
- '' +
+(function () {
+ 'use strict';
- '
',
+ angular.module('MassAutoComplete', [])
- link: function(scope, element) {
- scope.container = angular.element(element[0].getElementsByClassName('ac-container')[0]);
- },
+ .provider('massAutocompleteConfig', function () {
+ var config = this;
- controller: ['$scope', function($scope) {
- var that = this;
+ config.KEYS = {
+ TAB: 9,
+ ESC: 27,
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40
+ };
- var bound_events = {};
- bound_events[config.EVENTS.BLUR] = null;
- bound_events[config.EVENTS.KEYDOWN] = null;
- bound_events[config.EVENTS.RESIZE] = null;
+ config.EVENTS = {
+ KEYDOWN: 'keydown',
+ RESIZE: 'resize',
+ BLUR: 'blur'
+ };
- var _user_options = $scope.options() || {};
- var user_options = {
- debounce_position: _user_options.debounce_position || config.DEBOUNCE.position,
- debounce_attach: _user_options.debounce_attach || config.DEBOUNCE.attach,
- debounce_suggest: _user_options.debounce_suggest || config.DEBOUNCE.suggest,
- debounce_blur: _user_options.debounce_blur || config.DEBOUNCE.blur
+ config.DEBOUNCE = {
+ position: 150,
+ attach: 300,
+ suggest: 200,
+ blur: 150
};
- var current_element,
- current_model,
- current_options,
- previous_value,
- value_watch,
- last_selected_value,
- current_element_random_id_set;
-
- $scope.show_autocomplete = false;
-
- function show_autocomplete() {
- $scope.show_autocomplete = true;
- }
-
- function hide_autocomplete() {
- $scope.show_autocomplete = false;
- clear_selection();
- }
-
- // Debounce - taken from underscore.
- function debounce(func, wait, immediate) {
- var timeout;
- return function() {
- var context = this, args = arguments;
- var later = function() {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
- }
+ config.generate_random_id = function (prefix) {
+ return prefix + '_' + Math.random().toString().substring(2);
+ };
+
+ // Position ac container given a target element
+ config.position_autocomplete = function (container, target) {
+ var rect = target[0].getBoundingClientRect();
+
+ container[0].style.top = rect.height + 'px';
+ container[0].style.width = rect.width + 'px';
+ };
+
+ config.CLASSES = {
+ container: 'ac-container',
+ menu: 'ac-menu',
+ menu_item: 'ac-menu-item',
+ menu_item_focus: 'ac-state-focus'
+ };
+
+ this.$get = function () {
+ return config;
+ };
+ })
+
+ .directive('massAutocomplete', ['massAutocompleteConfig', '$timeout', '$window', '$document', '$q', function (config, $timeout, $window, $document, $q) {
+ return {
+ restrict: 'A',
+ scope: {
+ options: '&massAutocomplete'
+ },
+ transclude: true,
+ template: '' +
+
+ '' +
+
+ '' +
+
+ '
',
+
+ link: function (scope, element) {
+ element.addClass('mass-autocomplete');
+ scope.container = angular.element(element[0].getElementsByClassName('ac-container')[0]);
+ scope.container[0].style.position = 'absolute';
+ },
+
+ controller: ['$scope', function ($scope) {
+ var that = this;
+
+ var bound_events = {};
+ bound_events[config.EVENTS.BLUR] = null;
+ bound_events[config.EVENTS.KEYDOWN] = null;
+ bound_events[config.EVENTS.RESIZE] = null;
+
+ var _user_options = $scope.options() || {};
+ var user_options = {
+ debounce_position: _user_options.debounce_position || config.DEBOUNCE.position,
+ debounce_attach: _user_options.debounce_attach || config.DEBOUNCE.attach,
+ debounce_suggest: _user_options.debounce_suggest || config.DEBOUNCE.suggest,
+ debounce_blur: _user_options.debounce_blur || config.DEBOUNCE.blur
};
- var callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) {
- func.apply(context, args);
+
+ var current_element,
+ current_model,
+ current_options,
+ previous_value,
+ value_watch,
+ last_selected_value,
+ current_element_random_id_set;
+
+ $scope.show_autocomplete = false;
+
+ function show_autocomplete() {
+ $scope.show_autocomplete = true;
}
- };
- }
-
- // Make sure an element has id.
- // Return true if id was generated.
- function ensure_element_id(element) {
- if (!element.id || element.id === '') {
- element.id = config.generate_random_id('ac_element');
- return true;
- }
- return false;
- }
-
- function _position_autocomplete() {
- config.position_autocomplete($scope.container, current_element);
- }
- var position_autocomplete = debounce(_position_autocomplete, user_options.debounce_position);
-
- function _suggest(term, target_element) {
- $scope.selected_index = 0;
- $scope.waiting_for_suggestion = true;
-
- if (typeof(term) === 'string' && term.length > 0) {
- $q.when(current_options.suggest(term),
- function suggest_succeeded(suggestions) {
- // Make sure the suggestion we are processing is of the current element.
- // When using remote sources for example, a suggestion cycle might be
- // triggered at a later time (When a different field is in focus).
- if (!current_element || current_element !== target_element) {
- return;
+
+ function hide_autocomplete() {
+ $scope.show_autocomplete = false;
+ clear_selection();
+ }
+
+ // Debounce - taken from underscore.
+ function debounce(func, wait, immediate) {
+ var timeoutPromise;
+ return function () {
+ var context = this,
+ args = arguments;
+ var later = function () {
+ timeoutPromise = null;
+ if (!immediate) {
+ func.apply(context, args);
+ }
+ };
+ var callNow = immediate && !timeoutPromise;
+ $timeout.cancel(timeoutPromise);
+ timeoutPromise = $timeout(later, wait);
+ if (callNow) {
+ func.apply(context, args);
}
+ };
+ }
- if (suggestions && suggestions.length > 0) {
- // Set unique id to each suggestion so we can
- // reference them (aria)
- suggestions.forEach(function(s) {
- if (!s.id) {
- s.id = config.generate_random_id('ac_item');
+ // Make sure an element has id.
+ // Return true if id was generated.
+ function ensure_element_id(element) {
+ if (!element.id || element.id === '') {
+ element.id = config.generate_random_id('ac_element');
+ return true;
+ }
+ return false;
+ }
+
+ function _position_autocomplete() {
+ config.position_autocomplete($scope.container, current_element);
+ }
+ var position_autocomplete = debounce(_position_autocomplete, user_options.debounce_position);
+
+ function _suggest(term, target_element) {
+ $scope.selected_index = 0;
+ $scope.waiting_for_suggestion = true;
+
+ if (typeof (term) === 'string' && term.length > 0) {
+ $q.when(current_options.suggest(term),
+ function suggest_succeeded(suggestions) {
+ // Make sure the suggestion we are processing is of the current element.
+ // When using remote sources for example, a suggestion cycle might be
+ // triggered at a later time (When a different field is in focus).
+ if (!current_element || current_element !== target_element) {
+ return;
+ }
+
+ if (suggestions && suggestions.length > 0) {
+ // Set unique id to each suggestion so we can
+ // reference them (aria)
+ suggestions.forEach(function (s) {
+ if (!s.id) {
+ s.id = config.generate_random_id('ac_item');
+ }
+ });
+ // Add the original term as the first value to enable the user
+ // to return to his original expression after suggestions were made.
+ $scope.results = [{
+ value: term,
+ label: '',
+ id: ''
+ }].concat(suggestions);
+ show_autocomplete();
+ if (current_options.auto_select_first) {
+ set_selection(1);
+ }
+ } else {
+ $scope.results = [];
+ hide_autocomplete();
+ }
+ },
+ function suggest_failed(error) {
+ hide_autocomplete();
+ if (current_options.on_error) {
+ current_options.on_error(error);
}
- });
- // Add the original term as the first value to enable the user
- // to return to his original expression after suggestions were made.
- $scope.results = [{ value: term, label: '', id: ''}].concat(suggestions);
- show_autocomplete();
- if (current_options.auto_select_first) {
- set_selection(1);
}
- } else {
- $scope.results = [];
- hide_autocomplete();
- }
- },
- function suggest_failed(error) {
+ ).finally(function suggest_finally() {
+ $scope.waiting_for_suggestion = false;
+ });
+ } else {
+ $scope.waiting_for_suggestion = false;
hide_autocomplete();
- if (current_options.on_error) {
- current_options.on_error(error);
- }
+ $scope.$apply();
}
- ).finally(function suggest_finally() {
- $scope.waiting_for_suggestion = false;
- });
- } else {
- $scope.waiting_for_suggestion = false;
- hide_autocomplete();
- $scope.$apply();
- }
- }
- var suggest = debounce(_suggest, user_options.debounce_suggest);
-
- // Attach autocomplete behavior to an input element.
- function _attach(ngmodel, target_element, options) {
- // Element is already attached.
- if (current_element === target_element) {
- return;
- }
-
- // Safe: clear previously attached elements.
- if (current_element) {
- that.detach();
- }
+ }
+ var suggest = debounce(_suggest, user_options.debounce_suggest);
- // The element is still the active element.
- if (target_element[0] !== $document[0].activeElement) {
- return;
- }
+ // Attach autocomplete behavior to an input element.
+ function _attach(ngmodel, target_element, options) {
+ // Element is already attached.
+ if (current_element === target_element) {
+ return;
+ }
- if (options.on_attach) {
- options.on_attach();
- }
+ // Safe: clear previously attached elements.
+ if (current_element) {
+ that.detach();
+ }
- current_element = target_element;
- current_model = ngmodel;
- current_options = options;
- previous_value = ngmodel.$viewValue;
- current_element_random_id_set = ensure_element_id(target_element);
- $scope.container[0].setAttribute('aria-labelledby', current_element.id);
-
- $scope.results = [];
- $scope.selected_index = -1;
- bind_element();
-
- value_watch = $scope.$watch(
- function() {
- return ngmodel.$modelValue;
- },
- function(nv) {
- // Prevent suggestion cycle when the value is the last value selected.
- // When selecting from the menu the ng-model is updated and this watch
- // is triggered. This causes another suggestion cycle that will provide as
- // suggestion the value that is currently selected - this is unnecessary.
- if (nv === last_selected_value) {
+ // The element is still the active element.
+ if (target_element[0] !== $document[0].activeElement) {
return;
}
- _position_autocomplete();
- suggest(nv, current_element);
+ if (options.on_attach) {
+ options.on_attach();
+ }
+
+ current_element = target_element;
+ current_model = ngmodel;
+ current_options = options;
+ previous_value = ngmodel.$viewValue;
+ current_element_random_id_set = ensure_element_id(target_element);
+ $scope.container[0].setAttribute('aria-labelledby', current_element.id);
+
+ $scope.results = [];
+ $scope.selected_index = -1;
+ bind_element();
+
+ value_watch = $scope.$watch(
+ function () {
+ return ngmodel.$modelValue;
+ },
+ function (nv) {
+ // Prevent suggestion cycle when the value is the last value selected.
+ // When selecting from the menu the ng-model is updated and this watch
+ // is triggered. This causes another suggestion cycle that will provide as
+ // suggestion the value that is currently selected - this is unnecessary.
+ if (nv === last_selected_value) {
+ return;
+ }
+
+ _position_autocomplete();
+ suggest(nv, current_element);
+ }
+ );
}
- );
- }
- that.attach = debounce(_attach, user_options.debounce_attach);
-
- // Trigger end of editing and remove all attachments made by
- // this directive to the input element.
- that.detach = function() {
- if (current_element) {
- var value = current_element.val();
- update_model_value(value);
- if (current_options.on_detach) {
- current_options.on_detach(value);
+ that.attach = debounce(_attach, user_options.debounce_attach);
+
+ // Trigger end of editing and remove all attachments made by
+ // this directive to the input element.
+ that.detach = function () {
+ if (current_element) {
+ var value = current_element.val();
+ update_model_value(value);
+ if (current_options.on_detach) {
+ current_options.on_detach(value);
+ }
+ current_element.unbind(config.EVENTS.KEYDOWN, bound_events[config.EVENTS.KEYDOWN]);
+ current_element.unbind(config.EVENTS.BLUR, bound_events[config.EVENTS.BLUR]);
+ if (current_element_random_id_set) {
+ current_element[0].removeAttribute('id');
+ }
+ }
+ hide_autocomplete();
+ $scope.container[0].removeAttribute('aria-labelledby');
+ // Clear references and config.events.
+ angular.element($window).unbind(config.EVENTS.RESIZE, bound_events[config.EVENTS.RESIZE]);
+ if (value_watch) {
+ value_watch();
+ }
+ $scope.selected_index = $scope.results = undefined;
+ current_model = current_element = previous_value = undefined;
+ };
+
+ // Update angular's model view value.
+ // It is important that before triggering hooks the model's view
+ // value will be synced with the visible value to the user. This will
+ // allow the consumer controller to rely on its local ng-model.
+ function update_model_value(value) {
+ if (current_model.$modelValue !== value) {
+ current_model.$setViewValue(value);
+ current_model.$render();
+ }
}
- current_element.unbind(config.EVENTS.KEYDOWN, bound_events[config.EVENTS.KEYDOWN]);
- current_element.unbind(config.EVENTS.BLUR, bound_events[config.EVENTS.BLUR]);
- if (current_element_random_id_set) {
- current_element[0].removeAttribute('id');
+
+ function clear_selection() {
+ $scope.selected_index = -1;
+ $scope.container[0].removeAttribute('aria-activedescendant');
}
- }
- hide_autocomplete();
- $scope.container[0].removeAttribute('aria-labelledby');
- // Clear references and config.events.
- angular.element($window).unbind(config.EVENTS.RESIZE, bound_events[config.EVENTS.RESIZE]);
- if (value_watch) {
- value_watch();
- }
- $scope.selected_index = $scope.results = undefined;
- current_model = current_element = previous_value = undefined;
- };
- // Update angular's model view value.
- // It is important that before triggering hooks the model's view
- // value will be synced with the visible value to the user. This will
- // allow the consumer controller to rely on its local ng-model.
- function update_model_value(value) {
- if (current_model.$modelValue !== value) {
- current_model.$setViewValue(value);
- current_model.$render();
- }
- }
-
- function clear_selection() {
- $scope.selected_index = -1;
- $scope.container[0].removeAttribute('aria-activedescendant');
- }
-
- // Set the current selection while navigating through the menu.
- function set_selection(i) {
- // We use value instead of setting the model's view value
- // because we watch the model value and setting it will trigger
- // a new suggestion cycle.
- var selected = $scope.results[i];
- current_element.val(selected.value);
- $scope.selected_index = i;
- $scope.container[0].setAttribute('aria-activedescendant', selected.id);
- return selected;
- }
-
- // Apply and accept the current selection made from the menu.
- // When selecting from the menu directly (using click or touch) the
- // selection is directly applied.
- $scope.apply_selection = function(i) {
- current_element[0].focus();
- if (!$scope.show_autocomplete || i > $scope.results.length || i < 0) {
- return;
- }
+ // Set the scroll on selected position when keypress
+ function set_scroll(i) {
+ // only if container has scroll
+ if (angular.element($scope.container).get(0).scrollHeight > angular.element($scope.container).height()) {
+ var selected = $scope.results[i];
- var selected = set_selection(i);
- last_selected_value = selected.value;
- update_model_value(selected.value);
- hide_autocomplete();
+ if (selected.id !== '') {
+ var current_id_el = angular.element('#' + selected.id)[0];
+ angular.element($scope.container).scrollTop(current_id_el.offsetTop);
+ }
+ }
+ }
- if (current_options.on_select) {
- current_options.on_select(selected);
- }
- };
+ // Set the current selection while navigating through the menu.
+ function set_selection(i) {
+ // We use value instead of setting the model's view value
+ // because we watch the model value and setting it will trigger
+ // a new suggestion cycle.
+
+ var selected = $scope.results[i];
+ current_element.val(selected.value);
+ $scope.selected_index = i;
+ $scope.container[0].setAttribute('aria-activedescendant', selected.id);
+ return selected;
+ }
- function bind_element() {
- angular.element($window).bind(config.EVENTS.RESIZE, position_autocomplete);
+ $scope.apply_keyboard_selection = function (keyCode) {
+ if (keyCode == 13) { // press enter
+ var selected = set_selection(1);
+ last_selected_value = selected.value;
+ update_model_value(selected.value);
+ hide_autocomplete();
- bound_events[config.EVENTS.BLUR] = function() {
- // Detach the element from the auto complete when input loses focus.
- // Focus is lost when a selection is made from the auto complete menu
- // using the mouse (or touch). In that case we don't want to detach so
- // we wait several ms for the input to regain focus.
- $timeout(function() {
- if (!current_element || current_element[0] !== $document[0].activeElement) {
- that.detach();
+ if (current_options.on_select) {
+ current_options.on_select(selected);
+ }
}
- }, user_options.debounce_blur);
- };
- current_element.bind(config.EVENTS.BLUR, bound_events[config.EVENTS.BLUR]);
-
- bound_events[config.EVENTS.KEYDOWN] = function(e) {
- // Reserve key combinations with shift for different purposes.
- if (e.shiftKey) {
- return;
}
- switch (e.keyCode) {
- // Close the menu if it's open. Or, undo changes made to the value
- // if the menu is closed.
- case config.KEYS.ESC:
- if ($scope.show_autocomplete) {
- hide_autocomplete();
- $scope.$apply();
- } else {
- current_element.val(previous_value);
- }
- break;
-
- // Select an element and close the menu. Or, if a selection is
- // unavailable let the event propagate.
- case config.KEYS.ENTER:
- // Accept a selection only if results exist, the menu is
- // displayed and the results are valid (no current request
- // for new suggestions is active).
- if ($scope.show_autocomplete &&
- $scope.selected_index > 0 &&
- !$scope.waiting_for_suggestion) {
- $scope.apply_selection($scope.selected_index);
- // When selecting an item from the AC list the focus is set on
- // the input element. So the enter will cause a keypress event
- // on the input itself. Since this enter is not intended for the
- // input but for the AC result we prevent propagation to parent
- // elements because this event is not of their concern. We cannot
- // prevent events from firing when the event was registered on
- // the input itself.
- e.stopPropagation();
- e.preventDefault();
- }
+ // Apply and accept the current selection made from the menu.
+ // When selecting from the menu directly (using click or touch) the
+ // selection is directly applied.
+ $scope.apply_selection = function (i) {
+ current_element[0].focus();
+ if (!$scope.show_autocomplete || i > $scope.results.length || i < 0) {
+ return;
+ }
- hide_autocomplete();
- $scope.$apply();
- break;
+ var selected = set_selection(i);
+ last_selected_value = selected.value;
+ update_model_value(selected.value);
+ hide_autocomplete();
- // Navigate the menu when it's open. When it's not open fall back
- // to default behavior.
- case config.KEYS.TAB:
- if (!$scope.show_autocomplete) {
- break;
- }
+ if (current_options.on_select) {
+ current_options.on_select(selected);
+ }
+ };
- e.preventDefault();
-
- // Open the menu when results exists but are not displayed. Or,
- // select the next element when the menu is open. When reaching
- // bottom wrap to top.
- /* falls through */
- case config.KEYS.DOWN:
- if ($scope.results.length > 0) {
- if ($scope.show_autocomplete) {
- set_selection($scope.selected_index + 1 > $scope.results.length - 1 ? 0 : $scope.selected_index + 1);
- } else {
- show_autocomplete();
- set_selection(0);
+ function bind_element() {
+ angular.element($window).bind(config.EVENTS.RESIZE, position_autocomplete);
+
+ bound_events[config.EVENTS.BLUR] = function () {
+ // Detach the element from the auto complete when input loses focus.
+ // Focus is lost when a selection is made from the auto complete menu
+ // using the mouse (or touch). In that case we don't want to detach so
+ // we wait several ms for the input to regain focus.
+ $timeout(function () {
+ if (!current_element || current_element[0] !== $document[0].activeElement) {
+ that.detach();
}
- $scope.$apply();
+ }, user_options.debounce_blur);
+ };
+ current_element.bind(config.EVENTS.BLUR, bound_events[config.EVENTS.BLUR]);
+
+ bound_events[config.EVENTS.KEYDOWN] = function (e) {
+ // Reserve key combinations with shift for different purposes.
+ if (e.shiftKey) {
+ return;
}
- break;
-
- // Navigate up in the menu. When reaching the top wrap to bottom.
- case config.KEYS.UP:
- if ($scope.show_autocomplete) {
- e.preventDefault();
- set_selection($scope.selected_index - 1 >= 0 ? $scope.selected_index - 1 : $scope.results.length - 1);
- $scope.$apply();
+
+ var position;
+
+ switch (e.keyCode) {
+ // Close the menu if it's open. Or, undo changes made to the value
+ // if the menu is closed.
+ case config.KEYS.ESC:
+ if ($scope.show_autocomplete) {
+ hide_autocomplete();
+ $scope.$apply();
+ } else {
+ current_element.val(previous_value);
+ }
+ break;
+
+ // Select an element and close the menu. Or, if a selection is
+ // unavailable let the event propagate.
+ case config.KEYS.ENTER:
+ // Accept a selection only if results exist, the menu is
+ // displayed and the results are valid (no current request
+ // for new suggestions is active).
+ if ($scope.show_autocomplete &&
+ $scope.selected_index > 0 &&
+ !$scope.waiting_for_suggestion) {
+ $scope.apply_selection($scope.selected_index);
+ // When selecting an item from the AC list the focus is set on
+ // the input element. So the enter will cause a keypress event
+ // on the input itself. Since this enter is not intended for the
+ // input but for the AC result we prevent propagation to parent
+ // elements because this event is not of their concern. We cannot
+ // prevent events from firing when the event was registered on
+ // the input itself.
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ hide_autocomplete();
+ $scope.$apply();
+ break;
+
+ // Navigate the menu when it's open. When it's not open fall back
+ // to default behavior.
+ case config.KEYS.TAB:
+ if (!$scope.show_autocomplete) {
+ break;
+ }
+
+ e.preventDefault();
+
+ // Open the menu when results exists but are not displayed. Or,
+ // select the next element when the menu is open. When reaching
+ // bottom wrap to top.
+ /* falls through */
+ case config.KEYS.DOWN:
+ if ($scope.results.length > 0) {
+ if ($scope.show_autocomplete) {
+ position = $scope.selected_index + 1 > $scope.results.length - 1 ? 0 : $scope.selected_index + 1;
+ set_selection(position);
+ set_scroll(position);
+ } else {
+ show_autocomplete();
+ set_selection(0);
+ }
+ $scope.$apply();
+ }
+ break;
+
+ // Navigate up in the menu. When reaching the top wrap to bottom.
+ case config.KEYS.UP:
+ if ($scope.show_autocomplete) {
+ e.preventDefault();
+ position = $scope.selected_index - 1 >= 0 ? $scope.selected_index - 1 : $scope.results.length - 1;
+ set_selection(position);
+ set_scroll(position);
+ $scope.$apply();
+ }
+ break;
}
- break;
+ };
+ current_element.bind(config.EVENTS.KEYDOWN, bound_events[config.EVENTS.KEYDOWN]);
}
- };
- current_element.bind(config.EVENTS.KEYDOWN, bound_events[config.EVENTS.KEYDOWN]);
- }
-
- $scope.$on('$destroy', function() {
- that.detach();
- $scope.container.remove();
- });
- }]
- };
-}])
-
-.directive('massAutocompleteItem', function() {
- return {
- restrict: 'A',
- require: [
- '^massAutocomplete',
- 'ngModel'
- ],
- scope: {
- 'massAutocompleteItem' : '&'
- },
- link: function(scope, element, attrs, required) {
- // Prevent html5/browser auto completion.
- attrs.$set('autocomplete', 'off');
-
- var acContainer = required[0];
- var ngModel = required[1];
-
- element.bind('focus', function() {
- var options = scope.massAutocompleteItem();
- if (!options) {
- throw new Error('Invalid options');
+
+ $scope.$on('$destroy', function () {
+ that.detach();
+ $scope.container.remove();
+ });
+ }]
+ };
+ }])
+
+ .directive('massAutocompleteItem', function () {
+ return {
+ restrict: 'A',
+ require: [
+ '^massAutocomplete',
+ 'ngModel'
+ ],
+ scope: {
+ 'massAutocompleteItem': '&'
+ },
+ link: function (scope, element, attrs, required) {
+ // Prevent html5/browser auto completion.
+ attrs.$set('autocomplete', 'off');
+
+ var acContainer = required[0];
+ var ngModel = required[1];
+
+ element.bind('focus', function () {
+ var options = scope.massAutocompleteItem();
+ if (!options) {
+ throw new Error('Invalid options');
+ }
+ acContainer.attach(ngModel, element, options);
+ });
}
- acContainer.attach(ngModel, element, options);
- });
- }
- };
-});
-})();
+ };
+ });
+})();
\ No newline at end of file
diff --git a/massautocomplete.min.js b/massautocomplete.min.js
index 702b8d9..4e97e63 100644
--- a/massautocomplete.min.js
+++ b/massautocomplete.min.js
@@ -1 +1 @@
-!function(){"use strict";angular.module("MassAutoComplete",[]).provider("massAutocompleteConfig",function(){var e=this;e.KEYS={TAB:9,ESC:27,ENTER:13,UP:38,DOWN:40},e.EVENTS={KEYDOWN:"keydown",RESIZE:"resize",BLUR:"blur"},e.DEBOUNCE={position:150,attach:300,suggest:200,blur:150},e.generate_random_id=function(e){return e+"_"+Math.random().toString().substring(2)},e.position_autocomplete=function(e,t){var n=t[0].getBoundingClientRect(),o=document.body.scrollTop||document.documentElement.scrollTop||window.pageYOffset,i=document.body.scrollLeft||document.documentElement.scrollLeft||window.pageXOffset;e[0].style.top=n.top+n.height+o+"px",e[0].style.left=n.left+i+"px",e[0].style.width=n.width+"px"},this.$get=function(){return e}}).directive("massAutocomplete",["massAutocompleteConfig","$timeout","$window","$document","$q",function(e,t,n,o,i){return{restrict:"A",scope:{options:"&massAutocomplete"},transclude:!0,template:'',link:function(e,t){e.container=angular.element(t[0].getElementsByClassName("ac-container")[0])},controller:["$scope",function(a){function c(){a.show_autocomplete=!0}function l(){a.show_autocomplete=!1,m()}function s(e,t,n){var o;return function(){var i=this,a=arguments,c=function(){o=null,n||e.apply(i,a)},l=n&&!o;clearTimeout(o),o=setTimeout(c,t),l&&e.apply(i,a)}}function u(t){return(!t.id||""===t.id)&&(t.id=e.generate_random_id("ac_element"),!0)}function r(){e.position_autocomplete(a.container,b)}function d(t,n){a.selected_index=0,a.waiting_for_suggestion=!0,"string"==typeof t&&t.length>0?i.when(N.suggest(t),function(o){b&&b===n&&(o&&o.length>0?(o.forEach(function(t){t.id||(t.id=e.generate_random_id("ac_item"))}),a.results=[{value:t,label:"",id:""}].concat(o),c(),N.auto_select_first&&f(1)):(a.results=[],l()))},function(e){l(),N.on_error&&N.on_error(e)})["finally"](function(){a.waiting_for_suggestion=!1}):(a.waiting_for_suggestion=!1,l(),a.$apply())}function E(e,t,n){b!==t&&(b&&g.detach(),t[0]===o[0].activeElement&&(n.on_attach&&n.on_attach(),b=t,v=e,N=n,w=e.$viewValue,T=u(t),a.container[0].setAttribute("aria-labelledby",b.id),a.results=[],a.selected_index=-1,_(),S=a.$watch(function(){return e.$modelValue},function(e){e!==y&&(r(),A(e,b))})))}function p(e){v.$modelValue!==e&&(v.$setViewValue(e),v.$render())}function m(){a.selected_index=-1,a.container[0].removeAttribute("aria-activedescendant")}function f(e){var t=a.results[e];return b.val(t.value),a.selected_index=e,a.container[0].setAttribute("aria-activedescendant",t.id),t}function _(){angular.element(n).bind(e.EVENTS.RESIZE,x),h[e.EVENTS.BLUR]=function(){t(function(){b&&b[0]===o[0].activeElement||g.detach()},V.debounce_blur)},b.bind(e.EVENTS.BLUR,h[e.EVENTS.BLUR]),h[e.EVENTS.KEYDOWN]=function(t){if(!t.shiftKey)switch(t.keyCode){case e.KEYS.ESC:a.show_autocomplete?(l(),a.$apply()):b.val(w);break;case e.KEYS.ENTER:a.show_autocomplete&&a.selected_index>0&&!a.waiting_for_suggestion&&(a.apply_selection(a.selected_index),t.stopPropagation(),t.preventDefault()),l(),a.$apply();break;case e.KEYS.TAB:if(!a.show_autocomplete)break;t.preventDefault();case e.KEYS.DOWN:a.results.length>0&&(a.show_autocomplete?f(a.selected_index+1>a.results.length-1?0:a.selected_index+1):(c(),f(0)),a.$apply());break;case e.KEYS.UP:a.show_autocomplete&&(t.preventDefault(),f(a.selected_index-1>=0?a.selected_index-1:a.results.length-1),a.$apply())}},b.bind(e.EVENTS.KEYDOWN,h[e.EVENTS.KEYDOWN])}var g=this,h={};h[e.EVENTS.BLUR]=null,h[e.EVENTS.KEYDOWN]=null,h[e.EVENTS.RESIZE]=null;var b,v,N,w,S,y,T,$=a.options()||{},V={debounce_position:$.debounce_position||e.DEBOUNCE.position,debounce_attach:$.debounce_attach||e.DEBOUNCE.attach,debounce_suggest:$.debounce_suggest||e.DEBOUNCE.suggest,debounce_blur:$.debounce_blur||e.DEBOUNCE.blur};a.show_autocomplete=!1;var x=s(r,V.debounce_position),A=s(d,V.debounce_suggest);g.attach=s(E,V.debounce_attach),g.detach=function(){if(b){var t=b.val();p(t),N.on_detach&&N.on_detach(t),b.unbind(e.EVENTS.KEYDOWN,h[e.EVENTS.KEYDOWN]),b.unbind(e.EVENTS.BLUR,h[e.EVENTS.BLUR]),T&&b[0].removeAttribute("id")}l(),a.container[0].removeAttribute("aria-labelledby"),angular.element(n).unbind(e.EVENTS.RESIZE,h[e.EVENTS.RESIZE]),S&&S(),a.selected_index=a.results=void 0,v=b=w=void 0},a.apply_selection=function(e){if(b[0].focus(),!(!a.show_autocomplete||e>a.results.length||e<0)){var t=f(e);y=t.value,p(t.value),l(),N.on_select&&N.on_select(t)}},a.$on("$destroy",function(){g.detach(),a.container.remove()})}]}}]).directive("massAutocompleteItem",function(){return{restrict:"A",require:["^massAutocomplete","ngModel"],scope:{massAutocompleteItem:"&"},link:function(e,t,n,o){n.$set("autocomplete","off");var i=o[0],a=o[1];t.bind("focus",function(){var n=e.massAutocompleteItem();if(!n)throw new Error("Invalid options");i.attach(a,t,n)})}}})}();
\ No newline at end of file
+!function(){"use strict";angular.module("MassAutoComplete",[]).provider("massAutocompleteConfig",function(){var e=this;e.KEYS={TAB:9,ESC:27,ENTER:13,UP:38,DOWN:40},e.EVENTS={KEYDOWN:"keydown",RESIZE:"resize",BLUR:"blur"},e.DEBOUNCE={position:150,attach:300,suggest:200,blur:150},e.generate_random_id=function(e){return e+"_"+Math.random().toString().substring(2)},e.position_autocomplete=function(e,t){var n=t[0].getBoundingClientRect();e[0].style.top=n.height+"px",e[0].style.width=n.width+"px"},e.CLASSES={container:"ac-container",menu:"ac-menu",menu_item:"ac-menu-item",menu_item_focus:"ac-state-focus"},this.$get=function(){return e}}).directive("massAutocomplete",["massAutocompleteConfig","$timeout","$window","$document","$q",function(e,t,n,o,i){return{restrict:"A",scope:{options:"&massAutocomplete"},transclude:!0,template:'',link:function(e,t){t.addClass("mass-autocomplete"),e.container=angular.element(t[0].getElementsByClassName("ac-container")[0]),e.container[0].style.position="absolute"},controller:["$scope",function(a){function l(){a.show_autocomplete=!0}function c(){a.show_autocomplete=!1,_()}function s(e,n,o){var i;return function(){var a=this,l=arguments,c=function(){i=null,o||e.apply(a,l)},s=o&&!i;t.cancel(i),i=t(c,n),s&&e.apply(a,l)}}function u(t){return(!t.id||""===t.id)&&(t.id=e.generate_random_id("ac_element"),!0)}function r(){e.position_autocomplete(a.container,v)}function d(t,n){a.selected_index=0,a.waiting_for_suggestion=!0,"string"==typeof t&&t.length>0?i.when(N.suggest(t),function(o){v&&v===n&&(o&&o.length>0?(o.forEach(function(t){t.id||(t.id=e.generate_random_id("ac_item"))}),a.results=[{value:t,label:"",id:""}].concat(o),l(),N.auto_select_first&&f(1)):(a.results=[],c()))},function(e){c(),N.on_error&&N.on_error(e)})["finally"](function(){a.waiting_for_suggestion=!1}):(a.waiting_for_suggestion=!1,c(),a.$apply())}function E(e,t,n){v!==t&&(v&&h.detach(),t[0]===o[0].activeElement&&(n.on_attach&&n.on_attach(),v=t,S=e,N=n,y=e.$viewValue,A=u(t),a.container[0].setAttribute("aria-labelledby",v.id),a.results=[],a.selected_index=-1,g(),w=a.$watch(function(){return e.$modelValue},function(e){e!==$&&(r(),x(e,v))})))}function p(e){S.$modelValue!==e&&(S.$setViewValue(e),S.$render())}function _(){a.selected_index=-1,a.container[0].removeAttribute("aria-activedescendant")}function m(e){if(angular.element(a.container).get(0).scrollHeight>angular.element(a.container).height()){var t=a.results[e];if(""!==t.id){var n=angular.element("#"+t.id)[0];angular.element(a.container).scrollTop(n.offsetTop)}}}function f(e){var t=a.results[e];return v.val(t.value),a.selected_index=e,a.container[0].setAttribute("aria-activedescendant",t.id),t}function g(){angular.element(n).bind(e.EVENTS.RESIZE,C),b[e.EVENTS.BLUR]=function(){t(function(){v&&v[0]===o[0].activeElement||h.detach()},V.debounce_blur)},v.bind(e.EVENTS.BLUR,b[e.EVENTS.BLUR]),b[e.EVENTS.KEYDOWN]=function(t){if(!t.shiftKey){var n;switch(t.keyCode){case e.KEYS.ESC:a.show_autocomplete?(c(),a.$apply()):v.val(y);break;case e.KEYS.ENTER:a.show_autocomplete&&a.selected_index>0&&!a.waiting_for_suggestion&&(a.apply_selection(a.selected_index),t.stopPropagation(),t.preventDefault()),c(),a.$apply();break;case e.KEYS.TAB:if(!a.show_autocomplete)break;t.preventDefault();case e.KEYS.DOWN:a.results.length>0&&(a.show_autocomplete?(n=a.selected_index+1>a.results.length-1?0:a.selected_index+1,f(n),m(n)):(l(),f(0)),a.$apply());break;case e.KEYS.UP:a.show_autocomplete&&(t.preventDefault(),n=a.selected_index-1>=0?a.selected_index-1:a.results.length-1,f(n),m(n),a.$apply())}}},v.bind(e.EVENTS.KEYDOWN,b[e.EVENTS.KEYDOWN])}var h=this,b={};b[e.EVENTS.BLUR]=null,b[e.EVENTS.KEYDOWN]=null,b[e.EVENTS.RESIZE]=null;var v,S,N,y,w,$,A,T=a.options()||{},V={debounce_position:T.debounce_position||e.DEBOUNCE.position,debounce_attach:T.debounce_attach||e.DEBOUNCE.attach,debounce_suggest:T.debounce_suggest||e.DEBOUNCE.suggest,debounce_blur:T.debounce_blur||e.DEBOUNCE.blur};a.show_autocomplete=!1;var C=s(r,V.debounce_position),x=s(d,V.debounce_suggest);h.attach=s(E,V.debounce_attach),h.detach=function(){if(v){var t=v.val();p(t),N.on_detach&&N.on_detach(t),v.unbind(e.EVENTS.KEYDOWN,b[e.EVENTS.KEYDOWN]),v.unbind(e.EVENTS.BLUR,b[e.EVENTS.BLUR]),A&&v[0].removeAttribute("id")}c(),a.container[0].removeAttribute("aria-labelledby"),angular.element(n).unbind(e.EVENTS.RESIZE,b[e.EVENTS.RESIZE]),w&&w(),a.selected_index=a.results=void 0,S=v=y=void 0},a.apply_keyboard_selection=function(e){if(13==e){var t=f(1);$=t.value,p(t.value),c(),N.on_select&&N.on_select(t)}},a.apply_selection=function(e){if(v[0].focus(),!(!a.show_autocomplete||e>a.results.length||e<0)){var t=f(e);$=t.value,p(t.value),c(),N.on_select&&N.on_select(t)}},a.$on("$destroy",function(){h.detach(),a.container.remove()})}]}}]).directive("massAutocompleteItem",function(){return{restrict:"A",require:["^massAutocomplete","ngModel"],scope:{massAutocompleteItem:"&"},link:function(e,t,n,o){n.$set("autocomplete","off");var i=o[0],a=o[1];t.bind("focus",function(){var n=e.massAutocompleteItem();if(!n)throw new Error("Invalid options");i.attach(a,t,n)})}}})}();
\ No newline at end of file
diff --git a/massautocomplete.theme.css b/massautocomplete.theme.css
index bf24b8d..0efd3f5 100644
--- a/massautocomplete.theme.css
+++ b/massautocomplete.theme.css
@@ -1,3 +1,6 @@
+.mass-autocomplete {
+ position: relative;
+}
.ac-container {
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
diff --git a/package.json b/package.json
index 4ad92e0..175d648 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "angular-mass-autocomplete",
- "version": "0.4.0",
+ "version": "0.6.0",
"description": "Autocomplete for Angular.js applications with a lot to complete",
"main": "massautocomplete.js",
"repository": {