Skip to content

Commit

Permalink
fix(dropdown): Fix $digest:inprog on dropdown dismissal
Browse files Browse the repository at this point in the history
Make $apply first check if $rootScope is in $digest cycle before executing

Closes angular-ui#3274
  • Loading branch information
maxfierke authored and fernando-sendMail committed Jul 16, 2015
1 parent 458d2fa commit f815cd9
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 686 deletions.
320 changes: 0 additions & 320 deletions src/dropdown/dropdown.js
Original file line number Diff line number Diff line change
@@ -1,320 +0,0 @@
angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])

.constant('dropdownConfig', {
openClass: 'open'
})

.service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) {
var openScope = null;

this.open = function( dropdownScope ) {
if ( !openScope ) {
$document.bind('click', closeDropdown);
$document.bind('keydown', keybindFilter);
}

if ( openScope && openScope !== dropdownScope ) {
openScope.isOpen = false;
}

openScope = dropdownScope;
};

this.close = function( dropdownScope ) {
if ( openScope === dropdownScope ) {
openScope = null;
$document.unbind('click', closeDropdown);
$document.unbind('keydown', keybindFilter);
}
};

var closeDropdown = function( evt ) {
// This method may still be called during the same mouse event that
// unbound this event handler. So check openScope before proceeding.
if (!openScope) { return; }

if( evt && openScope.getAutoClose() === 'disabled' ) { return ; }

var toggleElement = openScope.getToggleElement();
if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) {
return;
}

var $element = openScope.getElement();
if( evt && openScope.getAutoClose() === 'outsideClick' && $element && $element[0].contains(evt.target) ) {
return;
}

openScope.isOpen = false;

if (!$rootScope.$$phase) {
openScope.$apply();
}
};

var keybindFilter = function( evt ) {
if ( evt.which === 27 ) {
openScope.focusToggleElement();
closeDropdown();
}
else if ( openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen ) {
evt.preventDefault();
evt.stopPropagation();
openScope.focusDropdownEntry(evt.which);
}
};
}])

.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) {
var self = this,
scope = $scope.$new(), // create a child scope so we are not polluting original one
templateScope,
openClass = dropdownConfig.openClass,
getIsOpen,
setIsOpen = angular.noop,
toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
appendToBody = false,
keynavEnabled =false,
selectedOption = null;

this.init = function( element ) {
self.$element = element;

if ( $attrs.isOpen ) {
getIsOpen = $parse($attrs.isOpen);
setIsOpen = getIsOpen.assign;

$scope.$watch(getIsOpen, function(value) {
scope.isOpen = !!value;
});
}

appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
keynavEnabled = angular.isDefined($attrs.keyboardNav);

if ( appendToBody && self.dropdownMenu ) {
$document.find('body').append( self.dropdownMenu );
element.on('$destroy', function handleDestroyEvent() {
self.dropdownMenu.remove();
});
}
};

this.toggle = function( open ) {
return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
};

// Allow other directives to watch status
this.isOpen = function() {
return scope.isOpen;
};

scope.getToggleElement = function() {
return self.toggleElement;
};

scope.getAutoClose = function() {
return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
};

scope.getElement = function() {
return self.$element;
};

scope.isKeynavEnabled = function() {
return keynavEnabled;
};

scope.focusDropdownEntry = function(keyCode) {
var elems = self.dropdownMenu ? //If append to body is used.
(angular.element(self.dropdownMenu).find('a')) :
(angular.element(self.$element).find('ul').eq(0).find('a'));

switch (keyCode) {
case (40): {
if ( !angular.isNumber(self.selectedOption)) {
self.selectedOption = 0;
} else {
self.selectedOption = (self.selectedOption === elems.length -1 ?
self.selectedOption :
self.selectedOption + 1);
}
break;
}
case (38): {
if ( !angular.isNumber(self.selectedOption)) {
return;
} else {
self.selectedOption = (self.selectedOption === 0 ?
0 :
self.selectedOption - 1);
}
break;
}
}
elems[self.selectedOption].focus();
};

scope.focusToggleElement = function() {
if ( self.toggleElement ) {
self.toggleElement[0].focus();
}
};

scope.$watch('isOpen', function( isOpen, wasOpen ) {
if ( appendToBody && self.dropdownMenu ) {
var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true);
self.dropdownMenu.css({
top: pos.top + 'px',
left: pos.left + 'px',
display: isOpen ? 'block' : 'none'
});
}

$animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass);

if ( isOpen ) {
if (self.dropdownMenuTemplateUrl) {
$templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
templateScope = scope.$new();
$compile(tplContent.trim())(templateScope, function(dropdownElement) {
var newEl = dropdownElement;
self.dropdownMenu.replaceWith(newEl);
self.dropdownMenu = newEl;
});
});
}

scope.focusToggleElement();
dropdownService.open( scope );
} else {
if (self.dropdownMenuTemplateUrl) {
if (templateScope) {
templateScope.$destroy();
}
var newEl = angular.element('<ul class="dropdown-menu"></ul>');
self.dropdownMenu.replaceWith(newEl);
self.dropdownMenu = newEl;
}

dropdownService.close( scope );
self.selectedOption = null;
}

setIsOpen($scope, isOpen);
if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
toggleInvoker($scope, { open: !!isOpen });
}
});

$scope.$on('$locationChangeSuccess', function() {
if (scope.getAutoClose() !== 'disabled') {
scope.isOpen = false;
}
});

$scope.$on('$destroy', function() {
scope.$destroy();
});
}])

.directive('dropdown', function() {
return {
controller: 'DropdownController',
link: function(scope, element, attrs, dropdownCtrl) {
dropdownCtrl.init( element );
}
};
})

.directive('dropdownMenu', function() {
return {
restrict: 'AC',
require: '?^dropdown',
link: function(scope, element, attrs, dropdownCtrl) {
if (!dropdownCtrl) {
return;
}
var tplUrl = attrs.templateUrl;
if (tplUrl) {
dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
}
if (!dropdownCtrl.dropdownMenu) {
dropdownCtrl.dropdownMenu = element;
}
}
};
})

.directive('keyboardNav', function() {
return {
restrict: 'A',
require: '?^dropdown',
link: function (scope, element, attrs, dropdownCtrl) {

element.bind('keydown', function(e) {

if ( /(38|40)/.test(e.which)) {

e.preventDefault();
e.stopPropagation();

var elems = angular.element(element).find('a');

switch (e.keyCode) {
case (40): { // Down
if ( !angular.isNumber(dropdownCtrl.selectedOption)) {
dropdownCtrl.selectedOption = 0;
} else {
dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === elems.length -1 ? dropdownCtrl.selectedOption : dropdownCtrl.selectedOption+1);
}

}
break;
case (38): { // Up
dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === 0 ? 0 : dropdownCtrl.selectedOption-1);
}
break;
}
elems[dropdownCtrl.selectedOption].focus();
}
});
}

};
})

.directive('dropdownToggle', function() {
return {
require: '?^dropdown',
link: function(scope, element, attrs, dropdownCtrl) {
if ( !dropdownCtrl ) {
return;
}

dropdownCtrl.toggleElement = element;

var toggleDropdown = function(event) {
event.preventDefault();

if ( !element.hasClass('disabled') && !attrs.disabled ) {
scope.$apply(function() {
dropdownCtrl.toggle();
});
}
};

element.bind('click', toggleDropdown);

// WAI-ARIA
element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
scope.$watch(dropdownCtrl.isOpen, function( isOpen ) {
element.attr('aria-expanded', !!isOpen);
});

scope.$on('$destroy', function() {
element.unbind('click', toggleDropdown);
});
}
};
});
Loading

0 comments on commit f815cd9

Please sign in to comment.