Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
refactor(tooltip, util): moved generic functions from tooltip to util
Browse files Browse the repository at this point in the history
* Refactored the tooltip a bit, made the code a lot approachable to read with adding updatePosition function that now is managing the whole tooltip positioning update
* Moved useful generic functions from the tooltip to the util

Closes #5419.
  • Loading branch information
EladBezalel authored and ThomasBurleson committed Nov 1, 2015
1 parent 5c129be commit 2df6a6a
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 50 deletions.
59 changes: 12 additions & 47 deletions src/components/tooltip/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe

$mdTheming(element);

var parent = getParentWithPointerEvents(),
var parent = $mdUtil.getParentWithPointerEvents(element),
content = angular.element(element[0].getElementsByClassName('md-content')[0]),
current = getNearestContentElement(),
current = $mdUtil.getNearestContentElement(element),
tooltipParent = angular.element(current || document.body),
debouncedOnResize = $$rAF.throttle(function () { if (scope.visible) positionTooltip(); });
debouncedOnResize = $$rAF.throttle(function () { updatePosition(); });

// Initialize element

Expand Down Expand Up @@ -103,7 +103,7 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe
else hideTooltip();
});

scope.$watch('direction', positionTooltip );
scope.$watch('direction', updatePosition );
}

function addAriaLabel () {
Expand All @@ -117,44 +117,6 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe
element.attr('role', 'tooltip');
}

/**
* Scan up dom hierarchy for enabled parent;
*/
function getParentWithPointerEvents () {
var parent = element.parent();

// jqLite might return a non-null, but still empty, parent; so check for parent and length
while (hasComputedStyleValue('pointer-events','none', parent)) {
parent = parent.parent();
}

return parent;
}

function getNearestContentElement () {
var current = element.parent()[0];
// Look for the nearest parent md-content, stopping at the rootElement.
while (current && current !== $rootElement[0] && current !== document.body && current.nodeName !== 'MD-CONTENT') {
current = current.parentNode;
}
return current;
}


function hasComputedStyleValue(key, value, target) {
var hasValue = false;

if ( target && target.length ) {
key = attr.$normalize(key);
target = target[0] || element[0];

var computedStyles = $window.getComputedStyle(target);
hasValue = angular.isDefined(computedStyles[key]) && (computedStyles[key] == value);
}

return hasValue;
}

function bindEvents () {
var mouseActive = false;

Expand Down Expand Up @@ -219,13 +181,13 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe

// Check if we should display it or not.
// This handles hide-* and show-* along with any user defined css
if ( hasComputedStyleValue('display','none') ) {
if ( $mdUtil.hasComputedStyle(element, 'display', 'none')) {
scope.visible = false;
element.detach();
return;
}

positionTooltip();
updatePosition();

angular.forEach([element, content], function (element) {
$animate.addClass(element, 'md-show');
Expand All @@ -246,9 +208,14 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe
});
}

function positionTooltip() {
function updatePosition() {
if ( !scope.visible ) return;

updateContentOrigin();
positionTooltip();
}

function positionTooltip() {
var tipRect = $mdUtil.offsetRect(element, tooltipParent);
var parentRect = $mdUtil.offsetRect(parent, tooltipParent);
var newPosition = getPosition(scope.direction);
Expand All @@ -261,8 +228,6 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe
newPosition = fitInParent(getPosition('top'));
}

updateContentOrigin();

element.css({
left: newPosition.left + 'px',
top: newPosition.top + 'px'
Expand Down
49 changes: 46 additions & 3 deletions src/core/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,30 @@ angular
.module('material.core')
.factory('$mdUtil', UtilFactory);

function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log) {
function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window) {
// Setup some core variables for the processTemplate method
var startSymbol = $interpolate.startSymbol(),
endSymbol = $interpolate.endSymbol(),
usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}'));

/**
* Checks if the target element has the requested style by key
* @param {DOMElement|JQLite} target Target element
* @param {string} key Style key
* @param {string=} expectedVal Optional expected value
* @returns {boolean} Whether the target element has the style or not
*/
var hasComputedStyle = function (target, key, expectedVal) {
var hasValue = false;

if ( target && target.length ) {
var computedStyles = $window.getComputedStyle(target[0]);
hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true);
}

return hasValue;
};

var $mdUtil = {
dom: {},
now: window.performance ?
Expand Down Expand Up @@ -249,7 +267,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
},
floatingScrollbars: function() {
if (this.floatingScrollbars.cached === undefined) {
var tempNode = angular.element('<div style="width: 100%; z-index: -1; position: absolute; height: 35px; overflow-y: scroll"><div style="height: 60;"></div></div>');
var tempNode = angular.element('<div style="width: 100%; z-index: -1; position: absolute; height: 35px; overflow-y: scroll"><div style="height: 60px;"></div></div>');
$document[0].body.appendChild(tempNode[0]);
this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
tempNode.remove();
Expand Down Expand Up @@ -633,7 +651,32 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
if (!template || !angular.isString(template)) return template;
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
}
}
},

/**
* Scan up dom hierarchy for enabled parent;
*/
getParentWithPointerEvents: function (element) {
var parent = element.parent();

// jqLite might return a non-null, but still empty, parent; so check for parent and length
while (hasComputedStyle(parent, 'pointer-events', 'none')) {
parent = parent.parent();
}

return parent;
},

getNearestContentElement: function (element) {
var current = element.parent()[0];
// Look for the nearest parent md-content, stopping at the rootElement.
while (current && current !== $rootElement[0] && current !== document.body && current.nodeName !== 'MD-CONTENT') {
current = current.parentNode;
}
return current;
},

hasComputedStyle: hasComputedStyle
};

// Instantiate other namespace utility methods
Expand Down
133 changes: 133 additions & 0 deletions src/core/util/util.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,139 @@ describe('util', function() {
}));
});

describe('hasComputedStyle', function () {
describe('with expected value', function () {
it('should return true for existing and matching value', inject(function($window, $mdUtil) {
spyOn($window, 'getComputedStyle').and.callFake(function() {
return { 'color': 'red' };
});

var elem = angular.element('<span style="color: red"></span>');

expect($mdUtil.hasComputedStyle(elem, 'color', 'red')).toBe(true);
}));

it('should return false for existing and not matching value', inject(function($window, $mdUtil) {
spyOn($window, 'getComputedStyle').and.callFake(function() {
return { 'color': 'red' };
});

var elem = angular.element('<span style="color: red"></span>');

expect($mdUtil.hasComputedStyle(elem, 'color', 'blue')).toBe(false);
}));
});

describe('without expected value', function () {
it('should return true for existing key', inject(function($window, $mdUtil) {
spyOn($window, 'getComputedStyle').and.callFake(function() {
return { 'color': 'red' };
});

var elem = angular.element('<span style="color: red"></span>');

expect($mdUtil.hasComputedStyle(elem, 'color')).toBe(true);
}));

it('should return false for not existing key', inject(function($window, $mdUtil) {
spyOn($window, 'getComputedStyle').and.callFake(function() {
return { 'color': 'red' };
});

var elem = angular.element('<span style="color: red"></span>');

expect($mdUtil.hasComputedStyle(elem, 'height')).toBe(false);
}));
});
});

describe('getParentWithPointerEvents', function () {
describe('with wrapper with pointer events style element', function () {
it('should find the parent element and return it', inject(function($window, $mdUtil) {
spyOn($window, 'getComputedStyle').and.callFake(function(elem) {
return angular.element(elem).css('pointer-events') ? { 'pointer-events': 'none' } : {};
});

var elem = angular.element('<span></span>');
var wrapper = angular.element('<div style="pointer-events: none;"></div>');
var parent = angular.element('<div></div>');

wrapper.append(elem);
parent.append(wrapper);

expect($mdUtil.getParentWithPointerEvents(elem)[0]).toBe(parent[0]);
}));
});

describe('with wrapper without pointer events style element', function () {
it('should find the wrapper element and return it', inject(function($window, $mdUtil) {
spyOn($window, 'getComputedStyle').and.callFake(function(elem) {
return {};
});

var elem = angular.element('<span></span>');
var wrapper = angular.element('<div id="wrapper"></div>');
var parent = angular.element('<div></div>');

wrapper.append(elem);
parent.append(wrapper);

expect($mdUtil.getParentWithPointerEvents(elem)[0]).toBe(wrapper[0]);
}));
});
});

describe('getNearestContentElement', function () {
describe('with rootElement as parent', function () {
it('should find stop at the rootElement and return it', inject(function($rootElement, $mdUtil) {
var elem = angular.element('<span></span>');
var wrapper = angular.element('<div></div>');

wrapper.append(elem);
$rootElement.append(wrapper);

expect($mdUtil.getNearestContentElement(elem)).toBe($rootElement[0]);
}));
});

describe('with document body as parent', function () {
it('should find stop at the document body and return it', inject(function($mdUtil) {
var elem = angular.element('<span></span>');
var wrapper = angular.element('<div></div>');
var body = angular.element(document.body);

wrapper.append(elem);
body.append(wrapper);

expect($mdUtil.getNearestContentElement(elem)).toBe(body[0]);
}));
});

describe('with md-content as parent', function () {
it('should find stop at md-content element and return it', inject(function($mdUtil) {
var elem = angular.element('<span></span>');
var wrapper = angular.element('<div></div>');
var content = angular.element('<md-content></md-content>');

wrapper.append(elem);
content.append(wrapper);

expect($mdUtil.getNearestContentElement(elem)).toBe(content[0]);
}));
});

describe('with no rootElement, body or md-content as parent', function () {
it('should return null', inject(function($mdUtil) {
var elem = angular.element('<span></span>');
var wrapper = angular.element('<div></div>');

wrapper.append(elem);

expect($mdUtil.getNearestContentElement(elem)).toBe(null);
}));
});
});

it('should use scope argument and `scope.$$destroyed` to skip the callback', inject(function($mdUtil) {
var callBackUsed, callback = function(){ callBackUsed = true; };
var scope = $rootScope.$new(true);
Expand Down

0 comments on commit 2df6a6a

Please sign in to comment.