From ae31079c01dedd9ed3a0805e78d092c98dfee813 Mon Sep 17 00:00:00 2001 From: Tasos Bekos Date: Sun, 19 Jan 2014 04:08:06 +0100 Subject: [PATCH] feat(dropdownToggle): support programmatic trigger & toggle callback Closes #270 Closes #284 Closes #1447 --- src/dropdownToggle/docs/demo.html | 60 ++++- src/dropdownToggle/docs/demo.js | 14 ++ src/dropdownToggle/docs/readme.md | 4 +- src/dropdownToggle/dropdownToggle.js | 130 +++++++---- .../test/dropdownToggle.spec.js | 206 +++++++++++++----- 5 files changed, 310 insertions(+), 104 deletions(-) diff --git a/src/dropdownToggle/docs/demo.html b/src/dropdownToggle/docs/demo.html index 5c12fe9708..eabd690f20 100644 --- a/src/dropdownToggle/docs/demo.html +++ b/src/dropdownToggle/docs/demo.html @@ -1,10 +1,50 @@ - + +
+ + + + Click me for a dropdown, yo! + + + + + +
+ + +
+ + +
+ + + +
+ +
+

+ +

+ +
diff --git a/src/dropdownToggle/docs/demo.js b/src/dropdownToggle/docs/demo.js index 8ece418d38..e3f2a834ad 100644 --- a/src/dropdownToggle/docs/demo.js +++ b/src/dropdownToggle/docs/demo.js @@ -4,4 +4,18 @@ function DropdownCtrl($scope) { 'And another choice for you.', 'but wait! A third!' ]; + + $scope.status = { + isopen: false + }; + + $scope.toggled = function(open) { + console.log('Dropdown is now: ', open); + }; + + $scope.toggleDropdown = function($event) { + $event.preventDefault(); + $event.stopPropagation(); + $scope.status.isopen = !$scope.status.isopen; + }; } diff --git a/src/dropdownToggle/docs/readme.md b/src/dropdownToggle/docs/readme.md index 2e42c46be6..e7bf152cf0 100644 --- a/src/dropdownToggle/docs/readme.md +++ b/src/dropdownToggle/docs/readme.md @@ -1,2 +1,4 @@ -DropdownToggle is a simple directive which will toggle a dropdown link on click. Simply put it on the `` tag of the toggler-element, and it will find the nearest dropdown menu and toggle it when the `` is clicked. +Dropdown is a simple directive which will toggle a dropdown menu on click or programmatically. +You can either use `is-open` to toggle or add inside a `` element to toggle it when is clicked. +There is also the `on-toggle(open)` optional expression fired when dropdown changes state. diff --git a/src/dropdownToggle/dropdownToggle.js b/src/dropdownToggle/dropdownToggle.js index 5a1510d7f9..3db0578e2b 100644 --- a/src/dropdownToggle/dropdownToggle.js +++ b/src/dropdownToggle/dropdownToggle.js @@ -1,52 +1,100 @@ -/* - * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js - * @restrict class or attribute - * @example: - - */ - -angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', function ($document) { - var openElement = null, - closeMenu = angular.noop; +angular.module('ui.bootstrap.dropdownToggle', []) + +.constant('dropdownConfig', { + openClass: 'open' +}) + +.service('dropdownService', ['$document', function($document) { + var self = this, openScope = null; + + this.open = function( dropdownScope ) { + if ( !openScope ) { + $document.bind('click', closeDropdown); + } + + if ( openScope && openScope !== dropdownScope ) { + openScope.isOpen = false; + } + + openScope = dropdownScope; + }; + + this.close = function( dropdownScope ) { + if ( openScope === dropdownScope ) { + openScope = null; + $document.unbind('click', closeDropdown); + } + }; + + var closeDropdown = function() { + openScope.$apply(function() { + openScope.isOpen = false; + }); + }; +}]) + +.controller('DropdownController', ['$scope', '$attrs', 'dropdownConfig', 'dropdownService', function($scope, $attrs, dropdownConfig, dropdownService) { + var self = this, openClass = dropdownConfig.openClass; + + this.init = function( element ) { + self.$element = element; + $scope.isOpen = angular.isDefined($attrs.isOpen) ? $scope.$parent.$eval($attrs.isOpen) : false; + }; + + this.toggle = function( open ) { + return $scope.isOpen = arguments.length ? !!open : !$scope.isOpen; + }; + + $scope.$watch('isOpen', function( value ) { + self.$element.toggleClass( openClass, value ); + + if ( value ) { + dropdownService.open( $scope ); + } else { + dropdownService.close( $scope ); + } + + $scope.onToggle({ open: !!value }); + }); + + $scope.$on('$locationChangeSuccess', function() { + $scope.isOpen = false; + }); +}]) + +.directive('dropdown', function() { return { restrict: 'CA', - link: function(scope, element, attrs) { - scope.$on('$locationChangeSuccess', function() { closeMenu(); }); - element.parent().bind('click', function() { closeMenu(); }); - element.bind('click', function (event) { + controller: 'DropdownController', + scope: { + isOpen: '=?', + onToggle: '&' + }, + link: function(scope, element, attrs, dropdownCtrl) { + dropdownCtrl.init( element ); + } + }; +}) - var elementWasOpen = (element === openElement); +.directive('dropdownToggle', function() { + return { + restrict: 'CA', + require: '?^dropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if ( !dropdownCtrl ) { + return; + } + element.bind('click', function(event) { event.preventDefault(); event.stopPropagation(); - if (!!openElement) { - closeMenu(); - } - - if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) { - element.parent().addClass('open'); - openElement = element; - closeMenu = function (event) { - if (event) { - event.preventDefault(); - event.stopPropagation(); - } - $document.unbind('click', closeMenu); - element.parent().removeClass('open'); - closeMenu = angular.noop; - openElement = null; - }; - $document.bind('click', closeMenu); + if ( !element.hasClass('disabled') && !element.prop('disabled') ) { + scope.$apply(function() { + dropdownCtrl.toggle(); + }); } }); } }; -}]); +}); diff --git a/src/dropdownToggle/test/dropdownToggle.spec.js b/src/dropdownToggle/test/dropdownToggle.spec.js index dec86fc01d..791d14fe20 100644 --- a/src/dropdownToggle/test/dropdownToggle.spec.js +++ b/src/dropdownToggle/test/dropdownToggle.spec.js @@ -1,74 +1,176 @@ describe('dropdownToggle', function() { - var $compile, $rootScope, $document, $location; + var $compile, $rootScope, $document, element; beforeEach(module('ui.bootstrap.dropdownToggle')); - beforeEach(inject(function(_$compile_, _$rootScope_, _$document_, _$location_) { + beforeEach(inject(function(_$compile_, _$rootScope_, _$document_) { $compile = _$compile_; $rootScope = _$rootScope_; $document = _$document_; - $location = _$location_; - })); - function dropdown() { - return $compile('')($rootScope); - } - - it('should toggle on `a` click', function() { - var elm = dropdown(); - expect(elm.hasClass('open')).toBe(false); - elm.find('a').click(); - expect(elm.hasClass('open')).toBe(true); + var clickDropdownToggle = function(elm) { + elm = elm || element; elm.find('a').click(); - expect(elm.hasClass('open')).toBe(false); - }); + }; - it('should toggle on `ul` click', function() { - var elm = dropdown(); - expect(elm.hasClass('open')).toBe(false); - elm.find('ul').click(); - expect(elm.hasClass('open')).toBe(true); - elm.find('ul').click(); - expect(elm.hasClass('open')).toBe(false); - }); + describe('basic', function() { + function dropdown() { + return $compile('')($rootScope); + } - it('should close on elm click', function() { - var elm = dropdown(); - elm.find('a').click(); - elm.click(); - expect(elm.hasClass('open')).toBe(false); + beforeEach(function() { + element = dropdown(); + }); + + it('should toggle on `a` click', function() { + expect(element.hasClass('open')).toBe(false); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(false); + }); + + it('should close on document click', function() { + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + $document.click(); + expect(element.hasClass('open')).toBe(false); + }); + + it('should close on $location change', function() { + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + $rootScope.$broadcast('$locationChangeSuccess'); + $rootScope.$apply(); + expect(element.hasClass('open')).toBe(false); + }); + + it('should only allow one dropdown to be open at once', function() { + var elm1 = dropdown(); + var elm2 = dropdown(); + expect(elm1.hasClass('open')).toBe(false); + expect(elm2.hasClass('open')).toBe(false); + + clickDropdownToggle( elm1 ); + expect(elm1.hasClass('open')).toBe(true); + expect(elm2.hasClass('open')).toBe(false); + + clickDropdownToggle( elm2 ); + expect(elm1.hasClass('open')).toBe(false); + expect(elm2.hasClass('open')).toBe(true); + }); + + it('should not toggle if the element has `disabled` class', function() { + var elm = $compile('')($rootScope); + clickDropdownToggle( elm ); + expect(elm.hasClass('open')).toBe(false); + }); + + it('should not toggle if the element is disabled', function() { + var elm = $compile('')($rootScope); + elm.find('button').click(); + expect(elm.hasClass('open')).toBe(false); + }); + + // issue 270 + it('executes other document click events normally', function() { + var checkboxEl = $compile('')($rootScope); + $rootScope.$digest(); + + expect(element.hasClass('open')).toBe(false); + expect($rootScope.clicked).toBeFalsy(); + + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + expect($rootScope.clicked).toBeFalsy(); + + checkboxEl.click(); + expect($rootScope.clicked).toBeTruthy(); + }); }); - it('should close on document click', function() { - var elm = dropdown(); - elm.find('a').click(); - $document.click(); - expect(elm.hasClass('open')).toBe(false); + describe('without trigger', function() { + beforeEach(function() { + $rootScope.isopen = true; + element = $compile('')($rootScope); + $rootScope.$digest(); + }); + + it('should be open initially', function() { + expect(element.hasClass('open')).toBe(true); + }); + + it('should toggle when `is-open` changes', function() { + $rootScope.isopen = false; + $rootScope.$digest(); + expect(element.hasClass('open')).toBe(false); + }); }); - it('should close on $location change', function() { - var elm = dropdown(); - elm.find('a').click(); - expect(elm.hasClass('open')).toBe(true); - $rootScope.$broadcast('$locationChangeSuccess'); - $rootScope.$apply(); - expect(elm.hasClass('open')).toBe(false); + describe('`is-open`', function() { + beforeEach(function() { + $rootScope.isopen = true; + element = $compile('')($rootScope); + $rootScope.$digest(); + }); + + it('should be open initially', function() { + expect(element.hasClass('open')).toBe(true); + }); + + it('should change `is-open` binding when toggles', function() { + clickDropdownToggle(); + expect($rootScope.isopen).toBe(false); + }); + + it('should toggle when `is-open` changes', function() { + $rootScope.isopen = false; + $rootScope.$digest(); + expect(element.hasClass('open')).toBe(false); + }); }); - it('should only allow one dropdown to be open at once', function() { - var elm1 = dropdown(); - var elm2 = dropdown(); - elm1.find('a').click(); - elm2.find('a').click(); - expect(elm1.hasClass('open')).toBe(false); - expect(elm2.hasClass('open')).toBe(true); + describe('`on-toggle`', function() { + beforeEach(function() { + $rootScope.toggleHandler = jasmine.createSpy('toggleHandler'); + element = $compile('')($rootScope); + $rootScope.$digest(); + }); + + it('should be called initially', function() { + expect($rootScope.toggleHandler).toHaveBeenCalledWith(false); + }); + + it('should call it correctly when toggles', function() { + clickDropdownToggle(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(true); + + clickDropdownToggle(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(false); + }); }); - it('should not toggle if the element is disabled', function() { - var elm = $compile('')($rootScope); - elm.find('a').click(); - expect(elm.hasClass('open')).toBe(false); + describe('`on-toggle` with initially open', function() { + beforeEach(function() { + $rootScope.toggleHandler = jasmine.createSpy('toggleHandler'); + $rootScope.isopen = true; + element = $compile('')($rootScope); + $rootScope.$digest(); + }); + + it('should be called initially with true', function() { + expect($rootScope.toggleHandler).toHaveBeenCalledWith(true); + }); + + it('should call it correctly when toggles', function() { + $rootScope.isopen = false; + $rootScope.$digest(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(false); + + $rootScope.isopen = true; + $rootScope.$digest(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(true); + }); }); }); -