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.
This commit is contained in:
Adam Bradley
2014-08-26 22:23:07 -05:00
parent b69aa5485f
commit 2c3f1c9f02
10 changed files with 203 additions and 45 deletions

View File

@@ -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

View File

@@ -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');
});
}

View File

@@ -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;

80
js/angular/service/body.js vendored Normal file
View File

@@ -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;
}
};
}]);

View File

@@ -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);

View File

@@ -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);
},

View File

@@ -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)();
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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>');
});
});