Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
fix(autocomplete): locks scrolling while autocomplete menu is visible
Browse files Browse the repository at this point in the history
Closes #2973
  • Loading branch information
Robert Messerle committed Jun 11, 2015
1 parent 33c3397 commit deae957
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 78 deletions.
137 changes: 73 additions & 64 deletions src/components/autocomplete/js/autocompleteController.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ var ITEM_HEIGHT = 41,
MENU_PADDING = 8;

function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $mdTheming, $window,
$animate, $rootElement) {
$animate, $rootElement, $attrs) {
//-- private variables
var self = this,
var ctrl = this,
itemParts = $scope.itemsExpr.split(/ in /i),
itemExpr = itemParts[1],
elements = null,
Expand All @@ -24,28 +24,30 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
defineProperty('hidden', handleHiddenChange, true);

//-- public variables
self.scope = $scope;
self.parent = $scope.$parent;
self.itemName = itemParts[0];
self.matches = [];
self.loading = false;
self.hidden = true;
self.index = null;
self.messages = [];
self.id = $mdUtil.nextUid();
ctrl.scope = $scope;
ctrl.parent = $scope.$parent;
ctrl.itemName = itemParts[0];
ctrl.matches = [];
ctrl.loading = false;
ctrl.hidden = true;
ctrl.index = null;
ctrl.messages = [];
ctrl.id = $mdUtil.nextUid();
ctrl.isDisabled = null;
ctrl.isRequired = null;

//-- public methods
self.keydown = keydown;
self.blur = blur;
self.focus = focus;
self.clear = clearValue;
self.select = select;
self.listEnter = onListEnter;
self.listLeave = onListLeave;
self.mouseUp = onMouseup;
self.getCurrentDisplayValue = getCurrentDisplayValue;
self.registerSelectedItemWatcher = registerSelectedItemWatcher;
self.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
ctrl.keydown = keydown;
ctrl.blur = blur;
ctrl.focus = focus;
ctrl.clear = clearValue;
ctrl.select = select;
ctrl.listEnter = onListEnter;
ctrl.listLeave = onListLeave;
ctrl.mouseUp = onMouseup;
ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;

return init();

Expand All @@ -55,6 +57,8 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
* Initialize the controller, setup watchers, gather elements
*/
function init () {
$mdUtil.initOptionalProperties($scope, $attrs, { searchText: null, selectedItem: null } );
$mdTheming($element);
configureWatchers();
$timeout(function () {
gatherElements();
Expand Down Expand Up @@ -129,9 +133,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
*/
function configureWatchers () {
var wait = parseInt($scope.delay, 10) || 0;
$scope.$watch('searchText', wait
? $mdUtil.debounce(handleSearchText, wait)
: handleSearchText);
$attrs.$observe('disabled', function (value) { ctrl.isDisabled = value; });
$attrs.$observe('required', function (value) { ctrl.isRequired = value !== null; });
$scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
registerSelectedItemWatcher(selectedItemChange);
$scope.$watch('selectedItem', handleSelectedItemChange);
angular.element($window).on('resize', positionDropdown);
Expand Down Expand Up @@ -195,6 +199,11 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
*/
function handleHiddenChange (hidden, oldHidden) {
if (!hidden && oldHidden) positionDropdown();
if (!hidden) {
if (elements) $mdUtil.disableScrollAround(elements.ul);
} else {
$mdUtil.enableScrolling();
}
}

/**
Expand All @@ -209,7 +218,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
*/
function onListLeave () {
noBlur = false;
if (!hasFocus) self.hidden = true;
if (!hasFocus) ctrl.hidden = true;
}

/**
Expand Down Expand Up @@ -271,7 +280,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
* @param previousSearchText
*/
function handleSearchText (searchText, previousSearchText) {
self.index = getDefaultIndex();
ctrl.index = getDefaultIndex();
//-- do nothing on init
if (searchText === previousSearchText) return;
//-- clear selected item if search text no longer matches it
Expand All @@ -282,9 +291,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
$scope.textChange(getItemScope($scope.selectedItem));
//-- cancel results if search text is not long enough
if (!isMinLengthMet()) {
self.loading = false;
self.matches = [];
self.hidden = shouldHide();
ctrl.loading = false;
ctrl.matches = [];
ctrl.hidden = shouldHide();
updateMessages();
} else {
handleQuery();
Expand All @@ -296,7 +305,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
*/
function blur () {
hasFocus = false;
if (!noBlur) self.hidden = true;
if (!noBlur) ctrl.hidden = true;
}

/**
Expand All @@ -307,8 +316,8 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
//-- if searchText is null, let's force it to be a string
if (!angular.isString($scope.searchText)) $scope.searchText = '';
if ($scope.minLength > 0) return;
self.hidden = shouldHide();
if (!self.hidden) handleQuery();
ctrl.hidden = shouldHide();
if (!ctrl.hidden) handleQuery();
}

/**
Expand All @@ -318,29 +327,29 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
function keydown (event) {
switch (event.keyCode) {
case $mdConstant.KEY_CODE.DOWN_ARROW:
if (self.loading) return;
if (ctrl.loading) return;
event.preventDefault();
self.index = Math.min(self.index + 1, self.matches.length - 1);
ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
updateScroll();
updateMessages();
break;
case $mdConstant.KEY_CODE.UP_ARROW:
if (self.loading) return;
if (ctrl.loading) return;
event.preventDefault();
self.index = self.index < 0 ? self.matches.length - 1 : Math.max(0, self.index - 1);
ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
updateScroll();
updateMessages();
break;
case $mdConstant.KEY_CODE.TAB:
case $mdConstant.KEY_CODE.ENTER:
if (self.hidden || self.loading || self.index < 0 || self.matches.length < 1) return;
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
event.preventDefault();
select(self.index);
select(ctrl.index);
break;
case $mdConstant.KEY_CODE.ESCAPE:
self.matches = [];
self.hidden = true;
self.index = getDefaultIndex();
ctrl.matches = [];
ctrl.hidden = true;
ctrl.index = getDefaultIndex();
break;
default:
}
Expand Down Expand Up @@ -373,7 +382,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
function getItemScope (item) {
if (!item) return;
var locals = {};
if (self.itemName) locals[self.itemName] = item;
if (ctrl.itemName) locals[ctrl.itemName] = item;
return locals;
}

Expand All @@ -398,7 +407,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
* @returns {*}
*/
function getCurrentDisplayValue () {
return getDisplayValue(self.matches[self.index]);
return getDisplayValue(ctrl.matches[ctrl.index]);
}

/**
Expand All @@ -418,7 +427,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
* @param value
*/
function defineProperty (key, handler, value) {
Object.defineProperty(self, key, {
Object.defineProperty(ctrl, key, {
get: function () { return value; },
set: function (newValue) {
var oldValue = value;
Expand All @@ -433,15 +442,15 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
* @param index
*/
function select (index) {
$scope.selectedItem = self.matches[index];
self.hidden = true;
self.index = 0;
self.matches = [];
$scope.selectedItem = ctrl.matches[index];
ctrl.hidden = true;
ctrl.index = 0;
ctrl.matches = [];
//-- force form to update state for validation
$timeout(function () {
elements.$.input.controller('ngModel').$setViewValue(getDisplayValue($scope.selectedItem) ||
$scope.searchText);
self.hidden = true;
ctrl.hidden = true;
});
}

Expand Down Expand Up @@ -470,18 +479,18 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
if (angular.isArray(items)) {
handleResults(items);
} else {
self.loading = true;
ctrl.loading = true;
if (items.success) items.success(handleResults);
if (items.then) items.then(handleResults);
if (items.error) items.error(function () { self.loading = false; });
if (items.error) items.error(function () { ctrl.loading = false; });
}
function handleResults (matches) {
cache[term] = matches;
if (searchText !== $scope.searchText) return; //-- just cache the results if old request
self.loading = false;
ctrl.loading = false;
promise = null;
self.matches = matches;
self.hidden = shouldHide();
ctrl.matches = matches;
ctrl.hidden = shouldHide();
updateMessages();
positionDropdown();
}
Expand All @@ -491,29 +500,29 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
* Updates the ARIA messages
*/
function updateMessages () {
self.messages = [ getCountMessage(), getCurrentDisplayValue() ];
ctrl.messages = [ getCountMessage(), getCurrentDisplayValue() ];
}

/**
* Returns the ARIA message for how many results match the current query.
* @returns {*}
*/
function getCountMessage () {
if (lastCount === self.matches.length) return '';
lastCount = self.matches.length;
switch (self.matches.length) {
if (lastCount === ctrl.matches.length) return '';
lastCount = ctrl.matches.length;
switch (ctrl.matches.length) {
case 0: return 'There are no matches available.';
case 1: return 'There is 1 match available.';
default: return 'There are ' + self.matches.length + ' matches available.';
default: return 'There are ' + ctrl.matches.length + ' matches available.';
}
}

/**
* Makes sure that the focused element is within view.
*/
function updateScroll () {
if (!elements.li[self.index]) return;
var li = elements.li[self.index],
if (!elements.li[ctrl.index]) return;
var li = elements.li[ctrl.index],
top = li.offsetTop,
bot = top + li.offsetHeight,
hgt = elements.ul.clientHeight;
Expand All @@ -538,12 +547,12 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $
}
//-- if results are cached, pull in cached results
if (!$scope.noCache && cache[term]) {
self.matches = cache[term];
ctrl.matches = cache[term];
updateMessages();
} else {
fetchResults(searchText);
}
if (hasFocus) self.hidden = shouldHide();
if (hasFocus) ctrl.hidden = shouldHide();
}

}
18 changes: 4 additions & 14 deletions src/components/autocomplete/js/autocompleteDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
return {
controller: 'MdAutocompleteCtrl',
controllerAs: '$mdAutocompleteCtrl',
link: link,
scope: {
inputName: '@mdInputName',
inputMinlength: '@mdInputMinlength',
Expand Down Expand Up @@ -142,12 +141,12 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
<ul role="presentation"\
class="md-autocomplete-suggestions md-whiteframe-z1 {{menuClass || \'\'}}"\
id="ul-{{$mdAutocompleteCtrl.id}}"\
ng-hide="$mdAutocompleteCtrl.hidden"\
ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
ng-mouseup="$mdAutocompleteCtrl.mouseUp()">\
<li ng-repeat="(index, item) in $mdAutocompleteCtrl.matches"\
ng-class="{ selected: index === $mdAutocompleteCtrl.index }"\
ng-hide="$mdAutocompleteCtrl.hidden"\
ng-click="$mdAutocompleteCtrl.select(index)"\
md-autocomplete-list-item="$mdAutocompleteCtrl.itemName">\
' + itemTemplate + '\
Expand Down Expand Up @@ -193,7 +192,7 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
ng-required="isRequired"\
ng-minlength="inputMinlength"\
ng-maxlength="inputMaxlength"\
ng-disabled="isDisabled"\
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
ng-blur="$mdAutocompleteCtrl.blur()"\
Expand All @@ -214,7 +213,7 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
ng-if="!floatingLabel"\
autocomplete="off"\
ng-required="isRequired"\
ng-disabled="isDisabled"\
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
ng-blur="$mdAutocompleteCtrl.blur()"\
Expand All @@ -229,7 +228,7 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
<button\
type="button"\
tabindex="-1"\
ng-if="$mdAutocompleteCtrl.scope.searchText && !isDisabled"\
ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\
ng-click="$mdAutocompleteCtrl.clear()">\
<md-icon md-svg-icon="md-close"></md-icon>\
<span class="md-visually-hidden">Clear</span>\
Expand All @@ -239,13 +238,4 @@ function MdAutocomplete ($mdTheming, $mdUtil) {
}
}
};

function link (scope, element, attr) {
attr.$observe('disabled', function (value) { scope.isDisabled = value; });
attr.$observe('required', function (value) { scope.isRequired = value !== null; });

$mdUtil.initOptionalProperties(scope, attr, {searchText:null, selectedItem:null} );

$mdTheming(element);
}
}

0 comments on commit deae957

Please sign in to comment.