Skip to content

Commit f815cd9

Browse files
maxfierkefernando-sendMail
authored andcommitted
fix(dropdown): Fix $digest:inprog on dropdown dismissal
Make $apply first check if $rootScope is in $digest cycle before executing Closes angular-ui#3274
1 parent 458d2fa commit f815cd9

File tree

2 files changed

+40
-686
lines changed

2 files changed

+40
-686
lines changed

Diff for: src/dropdown/dropdown.js

-320
Original file line numberDiff line numberDiff line change
@@ -1,320 +0,0 @@
1-
angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2-
3-
.constant('dropdownConfig', {
4-
openClass: 'open'
5-
})
6-
7-
.service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) {
8-
var openScope = null;
9-
10-
this.open = function( dropdownScope ) {
11-
if ( !openScope ) {
12-
$document.bind('click', closeDropdown);
13-
$document.bind('keydown', keybindFilter);
14-
}
15-
16-
if ( openScope && openScope !== dropdownScope ) {
17-
openScope.isOpen = false;
18-
}
19-
20-
openScope = dropdownScope;
21-
};
22-
23-
this.close = function( dropdownScope ) {
24-
if ( openScope === dropdownScope ) {
25-
openScope = null;
26-
$document.unbind('click', closeDropdown);
27-
$document.unbind('keydown', keybindFilter);
28-
}
29-
};
30-
31-
var closeDropdown = function( evt ) {
32-
// This method may still be called during the same mouse event that
33-
// unbound this event handler. So check openScope before proceeding.
34-
if (!openScope) { return; }
35-
36-
if( evt && openScope.getAutoClose() === 'disabled' ) { return ; }
37-
38-
var toggleElement = openScope.getToggleElement();
39-
if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) {
40-
return;
41-
}
42-
43-
var $element = openScope.getElement();
44-
if( evt && openScope.getAutoClose() === 'outsideClick' && $element && $element[0].contains(evt.target) ) {
45-
return;
46-
}
47-
48-
openScope.isOpen = false;
49-
50-
if (!$rootScope.$$phase) {
51-
openScope.$apply();
52-
}
53-
};
54-
55-
var keybindFilter = function( evt ) {
56-
if ( evt.which === 27 ) {
57-
openScope.focusToggleElement();
58-
closeDropdown();
59-
}
60-
else if ( openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen ) {
61-
evt.preventDefault();
62-
evt.stopPropagation();
63-
openScope.focusDropdownEntry(evt.which);
64-
}
65-
};
66-
}])
67-
68-
.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) {
69-
var self = this,
70-
scope = $scope.$new(), // create a child scope so we are not polluting original one
71-
templateScope,
72-
openClass = dropdownConfig.openClass,
73-
getIsOpen,
74-
setIsOpen = angular.noop,
75-
toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
76-
appendToBody = false,
77-
keynavEnabled =false,
78-
selectedOption = null;
79-
80-
this.init = function( element ) {
81-
self.$element = element;
82-
83-
if ( $attrs.isOpen ) {
84-
getIsOpen = $parse($attrs.isOpen);
85-
setIsOpen = getIsOpen.assign;
86-
87-
$scope.$watch(getIsOpen, function(value) {
88-
scope.isOpen = !!value;
89-
});
90-
}
91-
92-
appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
93-
keynavEnabled = angular.isDefined($attrs.keyboardNav);
94-
95-
if ( appendToBody && self.dropdownMenu ) {
96-
$document.find('body').append( self.dropdownMenu );
97-
element.on('$destroy', function handleDestroyEvent() {
98-
self.dropdownMenu.remove();
99-
});
100-
}
101-
};
102-
103-
this.toggle = function( open ) {
104-
return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
105-
};
106-
107-
// Allow other directives to watch status
108-
this.isOpen = function() {
109-
return scope.isOpen;
110-
};
111-
112-
scope.getToggleElement = function() {
113-
return self.toggleElement;
114-
};
115-
116-
scope.getAutoClose = function() {
117-
return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
118-
};
119-
120-
scope.getElement = function() {
121-
return self.$element;
122-
};
123-
124-
scope.isKeynavEnabled = function() {
125-
return keynavEnabled;
126-
};
127-
128-
scope.focusDropdownEntry = function(keyCode) {
129-
var elems = self.dropdownMenu ? //If append to body is used.
130-
(angular.element(self.dropdownMenu).find('a')) :
131-
(angular.element(self.$element).find('ul').eq(0).find('a'));
132-
133-
switch (keyCode) {
134-
case (40): {
135-
if ( !angular.isNumber(self.selectedOption)) {
136-
self.selectedOption = 0;
137-
} else {
138-
self.selectedOption = (self.selectedOption === elems.length -1 ?
139-
self.selectedOption :
140-
self.selectedOption + 1);
141-
}
142-
break;
143-
}
144-
case (38): {
145-
if ( !angular.isNumber(self.selectedOption)) {
146-
return;
147-
} else {
148-
self.selectedOption = (self.selectedOption === 0 ?
149-
0 :
150-
self.selectedOption - 1);
151-
}
152-
break;
153-
}
154-
}
155-
elems[self.selectedOption].focus();
156-
};
157-
158-
scope.focusToggleElement = function() {
159-
if ( self.toggleElement ) {
160-
self.toggleElement[0].focus();
161-
}
162-
};
163-
164-
scope.$watch('isOpen', function( isOpen, wasOpen ) {
165-
if ( appendToBody && self.dropdownMenu ) {
166-
var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true);
167-
self.dropdownMenu.css({
168-
top: pos.top + 'px',
169-
left: pos.left + 'px',
170-
display: isOpen ? 'block' : 'none'
171-
});
172-
}
173-
174-
$animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass);
175-
176-
if ( isOpen ) {
177-
if (self.dropdownMenuTemplateUrl) {
178-
$templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
179-
templateScope = scope.$new();
180-
$compile(tplContent.trim())(templateScope, function(dropdownElement) {
181-
var newEl = dropdownElement;
182-
self.dropdownMenu.replaceWith(newEl);
183-
self.dropdownMenu = newEl;
184-
});
185-
});
186-
}
187-
188-
scope.focusToggleElement();
189-
dropdownService.open( scope );
190-
} else {
191-
if (self.dropdownMenuTemplateUrl) {
192-
if (templateScope) {
193-
templateScope.$destroy();
194-
}
195-
var newEl = angular.element('<ul class="dropdown-menu"></ul>');
196-
self.dropdownMenu.replaceWith(newEl);
197-
self.dropdownMenu = newEl;
198-
}
199-
200-
dropdownService.close( scope );
201-
self.selectedOption = null;
202-
}
203-
204-
setIsOpen($scope, isOpen);
205-
if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
206-
toggleInvoker($scope, { open: !!isOpen });
207-
}
208-
});
209-
210-
$scope.$on('$locationChangeSuccess', function() {
211-
if (scope.getAutoClose() !== 'disabled') {
212-
scope.isOpen = false;
213-
}
214-
});
215-
216-
$scope.$on('$destroy', function() {
217-
scope.$destroy();
218-
});
219-
}])
220-
221-
.directive('dropdown', function() {
222-
return {
223-
controller: 'DropdownController',
224-
link: function(scope, element, attrs, dropdownCtrl) {
225-
dropdownCtrl.init( element );
226-
}
227-
};
228-
})
229-
230-
.directive('dropdownMenu', function() {
231-
return {
232-
restrict: 'AC',
233-
require: '?^dropdown',
234-
link: function(scope, element, attrs, dropdownCtrl) {
235-
if (!dropdownCtrl) {
236-
return;
237-
}
238-
var tplUrl = attrs.templateUrl;
239-
if (tplUrl) {
240-
dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
241-
}
242-
if (!dropdownCtrl.dropdownMenu) {
243-
dropdownCtrl.dropdownMenu = element;
244-
}
245-
}
246-
};
247-
})
248-
249-
.directive('keyboardNav', function() {
250-
return {
251-
restrict: 'A',
252-
require: '?^dropdown',
253-
link: function (scope, element, attrs, dropdownCtrl) {
254-
255-
element.bind('keydown', function(e) {
256-
257-
if ( /(38|40)/.test(e.which)) {
258-
259-
e.preventDefault();
260-
e.stopPropagation();
261-
262-
var elems = angular.element(element).find('a');
263-
264-
switch (e.keyCode) {
265-
case (40): { // Down
266-
if ( !angular.isNumber(dropdownCtrl.selectedOption)) {
267-
dropdownCtrl.selectedOption = 0;
268-
} else {
269-
dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === elems.length -1 ? dropdownCtrl.selectedOption : dropdownCtrl.selectedOption+1);
270-
}
271-
272-
}
273-
break;
274-
case (38): { // Up
275-
dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === 0 ? 0 : dropdownCtrl.selectedOption-1);
276-
}
277-
break;
278-
}
279-
elems[dropdownCtrl.selectedOption].focus();
280-
}
281-
});
282-
}
283-
284-
};
285-
})
286-
287-
.directive('dropdownToggle', function() {
288-
return {
289-
require: '?^dropdown',
290-
link: function(scope, element, attrs, dropdownCtrl) {
291-
if ( !dropdownCtrl ) {
292-
return;
293-
}
294-
295-
dropdownCtrl.toggleElement = element;
296-
297-
var toggleDropdown = function(event) {
298-
event.preventDefault();
299-
300-
if ( !element.hasClass('disabled') && !attrs.disabled ) {
301-
scope.$apply(function() {
302-
dropdownCtrl.toggle();
303-
});
304-
}
305-
};
306-
307-
element.bind('click', toggleDropdown);
308-
309-
// WAI-ARIA
310-
element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
311-
scope.$watch(dropdownCtrl.isOpen, function( isOpen ) {
312-
element.attr('aria-expanded', !!isOpen);
313-
});
314-
315-
scope.$on('$destroy', function() {
316-
element.unbind('click', toggleDropdown);
317-
});
318-
}
319-
};
320-
});

0 commit comments

Comments
 (0)