From 2c3f1c9f02ea3f2a90054556637a11f142010764 Mon Sep 17 00:00:00 2001 From: Adam Bradley <adambradley25@gmail.com> Date: Tue, 26 Aug 2014 22:23:07 -0500 Subject: [PATCH] feat($ionicBody): service to simplify body ele interaction Many services/directives have to interact with the body element, and each one has to write the same long code. The $ionicBody service provides some useful methods to clean up and reduce redundant code. --- js/angular/controller/sideMenuController.js | 18 ++--- js/angular/directive/sideMenus.js | 15 +--- js/angular/service/actionSheet.js | 10 +-- js/angular/service/body.js | 80 +++++++++++++++++++ js/angular/service/loading.js | 14 ++-- js/angular/service/modal.js | 10 +-- js/angular/service/popup.js | 10 +-- .../controller/sideMenuController.unit.js | 8 ++ test/unit/angular/directive/sideMenu.unit.js | 12 +++ test/unit/angular/service/body.unit.js | 71 ++++++++++++++++ 10 files changed, 203 insertions(+), 45 deletions(-) create mode 100644 js/angular/service/body.js create mode 100644 test/unit/angular/service/body.unit.js diff --git a/js/angular/controller/sideMenuController.js b/js/angular/controller/sideMenuController.js index 4c628b020f3..417ca3f4016 100644 --- a/js/angular/controller/sideMenuController.js +++ b/js/angular/controller/sideMenuController.js @@ -4,8 +4,8 @@ IonicModule '$attrs', '$ionicSideMenuDelegate', '$ionicPlatform', - '$document', -function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $document) { + '$ionicBody', +function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $ionicBody) { var self = this; var rightShowing, leftShowing, isDragging; var startX, lastX, offsetX, isAsideExposed; @@ -132,11 +132,9 @@ function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $document) { self.openAmount(self.right.width * p); } - if(percentage !== 0) { - $document[0].body.classList.add('menu-open'); - } else { - $document[0].body.classList.remove('menu-open'); - } + // add the CSS class "menu-open" if the percentage does not + // equal 0, otherwise remove the class from the body element + $ionicBody.enableClass( (percentage !== 0), 'menu-open'); }; /** @@ -269,11 +267,7 @@ function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $document) { }; self.activeAsideResizing = function(isResizing) { - if(isResizing) { - $document[0].body.classList.add('aside-resizing'); - } else { - $document[0].body.classList.remove('aside-resizing'); - } + $ionicBody.enableClass(isResizing, 'aside-resizing'); }; // End a drag with the given event diff --git a/js/angular/directive/sideMenus.js b/js/angular/directive/sideMenus.js index d008b35e71d..b8857174385 100644 --- a/js/angular/directive/sideMenus.js +++ b/js/angular/directive/sideMenus.js @@ -64,10 +64,7 @@ IonicModule * with {@link ionic.service:$ionicSideMenuDelegate}. * */ -.directive('ionSideMenus', ['$document', function($document) { - - var ASIDE_OPEN_CSS = 'aside-open'; - +.directive('ionSideMenus', ['$ionicBody', function($ionicBody) { return { restrict: 'ECA', controller: '$ionicSideMenus', @@ -76,21 +73,15 @@ IonicModule return { pre: prelink }; function prelink($scope) { - var bodyClassList = $document[0].body.classList; $scope.$on('$ionicExposeAside', function(evt, isAsideExposed){ if(!$scope.$exposeAside) $scope.$exposeAside = {}; $scope.$exposeAside.active = isAsideExposed; - if(isAsideExposed) { - bodyClassList.add(ASIDE_OPEN_CSS); - } else { - bodyClassList.remove(ASIDE_OPEN_CSS); - } + $ionicBody.enableClass(isAsideExposed, 'aside-open'); }); $scope.$on('$destroy', function(){ - bodyClassList.remove('menu-open'); - bodyClassList.remove(ASIDE_OPEN_CSS); + $ionicBody.removeClass('menu-open', 'aside-open'); }); } diff --git a/js/angular/service/actionSheet.js b/js/angular/service/actionSheet.js index f69a7366deb..17eabd26e67 100644 --- a/js/angular/service/actionSheet.js +++ b/js/angular/service/actionSheet.js @@ -51,13 +51,13 @@ IonicModule .factory('$ionicActionSheet', [ '$rootScope', - '$document', '$compile', '$animate', '$timeout', '$ionicTemplateLoader', '$ionicPlatform', -function($rootScope, $document, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform) { + '$ionicBody', +function($rootScope, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform, $ionicBody) { return { show: actionSheet @@ -119,7 +119,7 @@ function($rootScope, $document, $compile, $animate, $timeout, $ionicTemplateLoad scope.removed = true; sheetEl.removeClass('action-sheet-up'); - $document[0].body.classList.remove('action-sheet-open'); + $ionicBody.removeClass('action-sheet-open'); scope.$deregisterBackButton(); stateChangeListenDone(); @@ -135,8 +135,8 @@ function($rootScope, $document, $compile, $animate, $timeout, $ionicTemplateLoad scope.showSheet = function(done) { if (scope.removed) return; - $document[0].body.appendChild(element[0]); - $document[0].body.classList.add('action-sheet-open'); + $ionicBody.append(element) + .addClass('action-sheet-open'); $animate.addClass(element, 'active', function() { if (scope.removed) return; diff --git a/js/angular/service/body.js b/js/angular/service/body.js new file mode 100644 index 00000000000..05d4fd451af --- /dev/null +++ b/js/angular/service/body.js @@ -0,0 +1,80 @@ +/** + * @ngdoc service + * @name $ionicBody + * @module ionic + * @description An angular utility service to easily and efficiently + * add and remove CSS classes from the document's body element. + */ +IonicModule +.factory('$ionicBody', ['$document', function($document) { + return { + /** + * @ngdoc method + * @name $ionicBody#add + * @description Add a class to the document's body element. + * @param {string} class Each argument will be added to the body element. + * @returns {$ionicBody} The $ionicBody service so methods can be chained. + */ + addClass: function() { + for(var x=0; x<arguments.length; x++) { + $document[0].body.classList.add(arguments[x]); + } + return this; + }, + /** + * @ngdoc method + * @name $ionicBody#removeClass + * @description Remove a class from the document's body element. + * @param {string} class Each argument will be removed from the body element. + * @returns {$ionicBody} The $ionicBody service so methods can be chained. + */ + removeClass: function() { + for(var x=0; x<arguments.length; x++) { + $document[0].body.classList.remove(arguments[x]); + } + return this; + }, + /** + * @ngdoc method + * @name $ionicBody#enableClass + * @description Similar to the `add` method, except the first parameter accepts a boolean + * value determining if the class should be added or removed. Rather than writing user code, + * such as "if true then add the class, else then remove the class", this method can be + * given a true or false value which reduces redundant code. + * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed. + * @param {string} class Each remaining argument would be added or removed depending on + * the first argument. + * @returns {$ionicBody} The $ionicBody service so methods can be chained. + */ + enableClass: function(shouldEnableClass) { + var args = Array.prototype.slice.call(arguments).slice(1); + if(shouldEnableClass) { + this.addClass.apply(this, args); + } else { + this.removeClass.apply(this, args); + } + return this; + }, + /** + * @ngdoc method + * @name $ionicBody#append + * @description Append a child to the document's body. + * @param {element} element The element to be appended to the body. The passed in element + * can be either a jqLite element, or a DOM element. + * @returns {$ionicBody} The $ionicBody service so methods can be chained. + */ + append: function(ele) { + $document[0].body.appendChild( ele.length ? ele[0] : ele ); + return this; + }, + /** + * @ngdoc method + * @name $ionicBody#get + * @description Get the document's body element. + * @returns {element} Returns the document's body element. + */ + get: function() { + return $document[0].body; + } + }; +}]); diff --git a/js/angular/service/loading.js b/js/angular/service/loading.js index 13e81e59c3f..a2f7ab25921 100644 --- a/js/angular/service/loading.js +++ b/js/angular/service/loading.js @@ -58,7 +58,7 @@ IonicModule }) .factory('$ionicLoading', [ '$ionicLoadingConfig', - '$document', + '$ionicBody', '$ionicTemplateLoader', '$ionicBackdrop', '$timeout', @@ -66,7 +66,7 @@ IonicModule '$log', '$compile', '$ionicPlatform', -function($ionicLoadingConfig, $document, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform) { +function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform) { var loaderInstance; //default values @@ -104,7 +104,7 @@ function($ionicLoadingConfig, $document, $ionicTemplateLoader, $ionicBackdrop, $ if (!loaderInstance) { loaderInstance = $ionicTemplateLoader.compile({ template: LOADING_TPL, - appendTo: $document[0].body + appendTo: $ionicBody.get() }) .then(function(loader) { var self = loader; @@ -144,8 +144,10 @@ function($ionicLoadingConfig, $document, $ionicTemplateLoader, $ionicBackdrop, $ if (self.isShown) { self.element.addClass('visible'); ionic.requestAnimationFrame(function() { - self.isShown && self.element.addClass('active'); - self.isShown && $document[0].body.classList.add('loading-active'); + if(self.isShown) { + self.element.addClass('active'); + $ionicBody.addClass('loading-active'); + } }); } }); @@ -159,7 +161,7 @@ function($ionicLoadingConfig, $document, $ionicTemplateLoader, $ionicBackdrop, $ $ionicBackdrop.getElement().removeClass('backdrop-loading'); } self.element.removeClass('active'); - $document[0].body.classList.remove('loading-active'); + $ionicBody.removeClass('loading-active'); setTimeout(function() { !self.isShown && self.element.removeClass('visible'); }, 200); diff --git a/js/angular/service/modal.js b/js/angular/service/modal.js index 6e2863b7793..c3c7accc0a3 100644 --- a/js/angular/service/modal.js +++ b/js/angular/service/modal.js @@ -61,14 +61,14 @@ IonicModule .factory('$ionicModal', [ '$rootScope', - '$document', + '$ionicBody', '$compile', '$timeout', '$ionicPlatform', '$ionicTemplateLoader', '$q', '$log', -function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $q, $log) { +function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $q, $log) { /** * @ngdoc controller @@ -124,12 +124,12 @@ function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTempla self.el.classList.remove('hide'); $timeout(function(){ - $document[0].body.classList.add(self.viewType + '-open'); + $ionicBody.addClass(self.viewType + '-open'); }, 400); if(!self.el.parentElement) { modalEl.addClass(self.animation); - $document[0].body.appendChild(self.el); + $ionicBody.append(self.el); } if(target && self.positionView) { @@ -192,7 +192,7 @@ function($rootScope, $document, $compile, $timeout, $ionicPlatform, $ionicTempla ionic.views.Modal.prototype.hide.call(self); return $timeout(function(){ - $document[0].body.classList.remove(self.viewType + '-open'); + $ionicBody.removeClass(self.viewType + '-open'); self.el.classList.add('hide'); }, self.hideDelay || 500); }, diff --git a/js/angular/service/popup.js b/js/angular/service/popup.js index a9529a02299..96adc248a85 100644 --- a/js/angular/service/popup.js +++ b/js/angular/service/popup.js @@ -110,10 +110,10 @@ IonicModule '$q', '$timeout', '$rootScope', - '$document', + '$ionicBody', '$compile', '$ionicPlatform', -function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $document, $compile, $ionicPlatform) { +function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicBody, $compile, $ionicPlatform) { //TODO allow this to be configured var config = { stackPushDelay: 75 @@ -278,7 +278,7 @@ function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $docume var popupPromise = $ionicTemplateLoader.compile({ template: POPUP_TPL, scope: options.scope && options.scope.$new(), - appendTo: $document[0].body + appendTo: $ionicBody.get() }); var contentPromise = options.templateUrl ? $ionicTemplateLoader.load(options.templateUrl) : @@ -370,7 +370,7 @@ function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $docume .then(function(popup) { if (!previousPopup) { //Add popup-open & backdrop if this is first popup - document.body.classList.add('popup-open'); + $ionicBody.addClass('popup-open'); $ionicBackdrop.retain(); //only show the backdrop on the first popup $ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction( @@ -398,7 +398,7 @@ function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $docume previousPopup.show(); } else { //Remove popup-open & backdrop if this is last popup - document.body.classList.remove('popup-open'); + $ionicBody.removeClass('popup-open'); $ionicBackdrop.release(); ($ionicPopup._backButtonActionDone || angular.noop)(); } diff --git a/test/unit/angular/controller/sideMenuController.unit.js b/test/unit/angular/controller/sideMenuController.unit.js index ff9e20dd981..9ec6706b243 100644 --- a/test/unit/angular/controller/sideMenuController.unit.js +++ b/test/unit/angular/controller/sideMenuController.unit.js @@ -84,6 +84,14 @@ describe('$ionicSideMenus controller', function() { expect(ctrl.getOpenPercentage()).toEqual(-50); }); + it('should add/remove menu-open from the body class', inject(function($document) { + expect($document[0].body.classList.contains('menu-open')).toEqual(false); + ctrl.openPercentage(100); + expect($document[0].body.classList.contains('menu-open')).toEqual(true); + ctrl.openPercentage(0); + expect($document[0].body.classList.contains('menu-open')).toEqual(false); + })); + // Open it('should toggle left', function() { ctrl.toggleLeft(); diff --git a/test/unit/angular/directive/sideMenu.unit.js b/test/unit/angular/directive/sideMenu.unit.js index 0892f90bf1a..0a048f7605e 100644 --- a/test/unit/angular/directive/sideMenu.unit.js +++ b/test/unit/angular/directive/sideMenu.unit.js @@ -40,6 +40,18 @@ describe('Ionic Angular Side Menu', function() { expect(sideMenuController.isAsideExposed()).toBe(false); })); + it('should add/remove "aside-resizing" from the body tag when using activeAsideResizing', inject(function($compile, $rootScope, $document) { + var el = $compile('<ion-side-menus><ion-side-menu></><ion-side-menu-content></ion-side-menu-content></ion-side-menus>')($rootScope.$new()); + $rootScope.$apply(); + var sideMenuController = el.controller('ionSideMenus'); + + expect($document[0].body.classList.contains('aside-resizing')).toEqual(false); + sideMenuController.activeAsideResizing(true); + expect($document[0].body.classList.contains('aside-resizing')).toEqual(true); + sideMenuController.activeAsideResizing(false); + expect($document[0].body.classList.contains('aside-resizing')).toEqual(false); + })); + it('should emit $ionicexposeAside', inject(function($compile, $rootScope) { var el = $compile('<ion-side-menus><ion-side-menu></><ion-side-menu-content></ion-side-menu-content></ion-side-menus>')($rootScope.$new()); $rootScope.$apply(); diff --git a/test/unit/angular/service/body.unit.js b/test/unit/angular/service/body.unit.js new file mode 100644 index 00000000000..493f845e944 --- /dev/null +++ b/test/unit/angular/service/body.unit.js @@ -0,0 +1,71 @@ +describe('Ionic Body Class Service', function() { + var ionicBody, doc; + + beforeEach(module('ionic')); + + beforeEach(inject(function($ionicBody, $document) { + ionicBody = $ionicBody; + body = $document[0].body; + })); + + it('Should add/remove one class the body tag', function() { + ionicBody.addClass('class1'); + expect(body.classList.contains('class1')).toEqual(true); + ionicBody.removeClass('class1'); + expect(body.classList.contains('class1')).toEqual(false); + }); + + it('Should add/remove multiple classes the body tag', function() { + ionicBody.addClass('class1', 'class2', 'class3'); + expect(body.classList.contains('class1')).toEqual(true); + expect(body.classList.contains('class2')).toEqual(true); + expect(body.classList.contains('class3')).toEqual(true); + ionicBody.removeClass('class1', 'class2', 'class3'); + expect(body.classList.contains('class1')).toEqual(false); + expect(body.classList.contains('class2')).toEqual(false); + expect(body.classList.contains('class3')).toEqual(false); + }); + + it('Should add/remove classes by chaining methods', function() { + ionicBody.addClass('class1').addClass('class2').addClass('class3'); + expect(body.classList.contains('class1')).toEqual(true); + expect(body.classList.contains('class2')).toEqual(true); + expect(body.classList.contains('class3')).toEqual(true); + ionicBody.removeClass('class1').removeClass('class2').removeClass('class3'); + expect(body.classList.contains('class1')).toEqual(false); + expect(body.classList.contains('class2')).toEqual(false); + expect(body.classList.contains('class3')).toEqual(false); + }); + + it('Should add/false one class using addTo and boolean first arg', function() { + ionicBody.enableClass(true, 'class1'); + expect(body.classList.contains('class1')).toEqual(true); + ionicBody.enableClass(false, 'class1'); + expect(body.classList.contains('class1')).toEqual(false); + }); + + it('Should add/false multiple classes using addTo and boolean first arg', function() { + ionicBody.enableClass(true, 'class1', 'class2', 'class3'); + expect(body.classList.contains('class1')).toEqual(true); + expect(body.classList.contains('class2')).toEqual(true); + expect(body.classList.contains('class3')).toEqual(true); + ionicBody.enableClass(false, 'class1', 'class2', 'class3'); + expect(body.classList.contains('class1')).toEqual(false); + expect(body.classList.contains('class2')).toEqual(false); + expect(body.classList.contains('class3')).toEqual(false); + }); + + it('Should append a jqLite element to the body', function() { + var jqLiteElement = angular.element('<div>jqLite</div>'); + ionicBody.append(jqLiteElement); + expect(body.lastChild.outerHTML).toEqual('<div>jqLite</div>'); + }); + + it('Should append a DOM element to the body', function() { + var domElement = document.createElement('span'); + domElement.innerText = 'domElement' + ionicBody.append(domElement); + expect(body.lastChild.outerHTML).toEqual('<span>domElement</span>'); + }); + +});