diff --git a/docs/config/template/index.template.html b/docs/config/template/index.template.html index 27862abe333..f74dd6314a6 100644 --- a/docs/config/template/index.template.html +++ b/docs/config/template/index.template.html @@ -28,19 +28,19 @@

- - + md-active="menu.isPageSelected(section, page)" + md-ink-ripple="#bbb"> diff --git a/src/components/button/button.js b/src/components/button/button.js index 3ba8cd5c648..4db851940b1 100644 --- a/src/components/button/button.js +++ b/src/components/button/button.js @@ -68,7 +68,7 @@ function MdButtonDirective($mdInkRipple, $mdTheming, $mdAria) { function postLink(scope, element, attr) { var node = element[0]; $mdTheming(element); - $mdInkRipple.attachButtonBehavior(element); + $mdInkRipple.attachButtonBehavior(scope, element); var elementHasText = node.textContent.trim(); if (!elementHasText) { diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js index 68d2e92f871..28d4122ea0d 100644 --- a/src/components/checkbox/checkbox.js +++ b/src/components/checkbox/checkbox.js @@ -55,7 +55,7 @@ function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant, transclude: true, require: '?ngModel', template: - '
' + + '
' + '
' + '
' + '
', diff --git a/src/components/radioButton/radioButton.js b/src/components/radioButton/radioButton.js index b59ba08b7f0..2c5ae810ed6 100644 --- a/src/components/radioButton/radioButton.js +++ b/src/components/radioButton/radioButton.js @@ -197,7 +197,7 @@ function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { restrict: 'E', require: '^mdRadioGroup', transclude: true, - template: '
' + + template: '
' + '
' + '
' + '
' + diff --git a/src/components/tabs/js/tabItemDirective.js b/src/components/tabs/js/tabItemDirective.js index 36a0d3cdae0..361296f9e80 100644 --- a/src/components/tabs/js/tabItemDirective.js +++ b/src/components/tabs/js/tabItemDirective.js @@ -94,7 +94,7 @@ function MdTabDirective($mdInkRipple, $compile, $mdAria, $mdUtil, $mdConstant) { transcludeTabContent(); configureAria(); - var detachRippleFn = $mdInkRipple.attachButtonBehavior(element); + var detachRippleFn = $mdInkRipple.attachButtonBehavior(scope, element); tabsCtrl.add(tabItemCtrl); scope.$on('$destroy', function() { detachRippleFn(); diff --git a/src/core/services/ripple/ripple.js b/src/core/services/ripple/ripple.js index a0990ecabc8..32d6905a10d 100644 --- a/src/core/services/ripple/ripple.js +++ b/src/core/services/ripple/ripple.js @@ -8,15 +8,18 @@ angular.module('material.core') .directive('mdNoBar', attrNoDirective()) .directive('mdNoStretch', attrNoDirective()); -function InkRippleDirective($mdInkRipple) { - return function(scope, element, attr) { - if (attr.mdInkRipple == 'checkbox') { - $mdInkRipple.attachCheckboxBehavior(element); - } else { - $mdInkRipple.attachButtonBehavior(element); - } - }; -} + function InkRippleDirective($mdInkRipple) { + return { + controller: angular.noop, + link: function (scope, element, attr) { + if (attr.hasOwnProperty('mdInkRippleCheckbox')) { + $mdInkRipple.attachCheckboxBehavior(scope, element); + } else { + $mdInkRipple.attachButtonBehavior(scope, element); + } + } + }; + } function InkRippleService($window, $timeout) { @@ -26,27 +29,34 @@ function InkRippleService($window, $timeout) { attach: attach }; - function attachButtonBehavior(element) { - return attach(element, { + function attachButtonBehavior(scope, element) { + return attach(scope, element, { center: element.hasClass('md-fab'), dimBackground: true }); } - function attachCheckboxBehavior(element) { - return attach(element, { + function attachCheckboxBehavior(scope, element) { + return attach(scope, element, { center: true, dimBackground: false }); } - function attach(element, options) { - + function attach(scope, element, options) { if (element.controller('mdNoInk')) return angular.noop; - var rippleContainer, rippleEl, + var rippleContainer, + controller = element.controller('mdInkRipple') || {}, + counter = 0, + ripples = [], + states = [], + isActiveExpr = element.attr('md-active'), + isActive = false, + isHeld = false, node = element[0], hammertime = new Hammer(node), + color = parseColor(element.attr('md-ink-ripple')) || parseColor($window.getComputedStyle(node).color || 'rgb(0, 0, 0)'), contentParent = element.controller('mdContent'); options = angular.extend({ @@ -60,107 +70,215 @@ function InkRippleService($window, $timeout) { options.mousedown && hammertime.on('hammer.input', onInput); + controller.createRipple = createRipple; + + if (isActiveExpr) { + scope.$watch( + function () { + return scope.$eval(isActiveExpr); + }, + function (newValue) { + isActive = newValue; + if (isActive) { + if (ripples.length === 0) { + createRipple(0, 0); + } + } + angular.forEach(ripples, updateElement); + } + ); + } + // Publish self-detach method if desired... return function detach() { hammertime.destroy(); rippleContainer && rippleContainer.remove(); }; - function rippleIsAllowed() { - var parent; - return !element[0].hasAttribute('disabled') && - !((parent = element[0].parentNode) && parent.hasAttribute('disabled')); - } - - function removeElement(element, wait) { - $timeout(function () { - element.remove(); - }, wait, false); - } + function parseColor(color) { + if (!color) return; + if (color.indexOf('rgba') === 0) return color; + if (color.indexOf('rgb') === 0) return rgbToRGBA(color); + if (color.indexOf('#') === 0) return hexToRGBA(color); - function createRipple(left, top, positionsAreAbsolute) { + /** + * + */ + function hexToRGBA(color) { + var hex = color.charAt(0) === '#' ? color.substr(1) : color, + dig = hex.length / 3, + red = hex.substr(0, dig), + grn = hex.substr(dig, dig), + blu = hex.substr(dig * 2); + if (dig === 1) { + red += red; + grn += grn; + blu += blu; + } + return 'rgba(' + parseInt(red, 16) + ',' + parseInt(grn, 16) + ',' + parseInt(blu, 16) + ',0.1)'; + } - var rippleEl = angular.element('
'); + /** + * + */ + function rgbToRGBA(color) { + return color.replace(')', ', 0.1)').replace('(', 'a(') + } - if (!rippleContainer) { - rippleContainer = angular.element('
'); - element.append(rippleContainer); } - rippleContainer.append(rippleEl); - - var containerWidth = rippleContainer.prop('offsetWidth'), - containerHeight = rippleContainer.prop('offsetHeight'), - multiplier = element.hasClass('md-fab') ? 1.1 : 0.8, - rippleWidth = Math.max(containerWidth, containerHeight) * multiplier; - if (contentParent) { - top += contentParent.$element.prop('scrollTop'); + function removeElement(elem, wait) { + ripples.splice(ripples.indexOf(elem), 1); + if (ripples.length === 0) { + rippleContainer && rippleContainer.css({ backgroundColor: '' }); } + $timeout(function () { elem.remove(); }, wait, false); + } - var css = { - backgroundColor: $window.getComputedStyle(rippleEl[0]).color || $window.getComputedStyle(node).color, - width: rippleWidth + 'px', - height: rippleWidth + 'px', - marginLeft: (rippleWidth * -0.5) + 'px', - marginTop: (rippleWidth * -0.5) + 'px' - }; - - if (options.center) { - css.left = '50%'; - css.top = '50%'; - } else if (positionsAreAbsolute) { - var elementRect = node.getBoundingClientRect(); - left -= elementRect.left; - top -= elementRect.top; - css.left = Math.round(left / containerWidth * 100) + '%'; - css.top = Math.round(top / containerHeight * 100) + '%'; + function updateElement(elem) { + var index = ripples.indexOf(elem), + state = states[index], + elemIsActive = ripples.length > 1 ? false : isActive, + elemIsHeld = ripples.length > 1 ? false : isHeld; + if (elemIsActive || state.animating || elemIsHeld) { + elem.addClass('md-ripple-visible'); + } else { + elem.removeClass('md-ripple-visible'); + removeElement(elem, 650); } + } + + /** + * + * @returns {*} + */ + function createRipple(left, top) { - rippleEl.css(css); + var container = getRippleContainer(), + size = getRippleSize(), + css = getRippleCss(size, left, top), + elem = getRippleElement(css), + index = ripples.indexOf(elem), + state = states[index]; + + state.animating = true; - //-- Use minimum timeout to trigger CSS animation $timeout(function () { if (options.dimBackground) { - rippleContainer.addClass('md-ripple-full md-ripple-visible'); - rippleContainer.css({ backgroundColor: css.backgroundColor.replace(')', ', 0.1').replace('(', 'a(') }); + container.css({ backgroundColor: color }); } - rippleEl.addClass('md-ripple-placed md-ripple-visible md-ripple-scaled md-ripple-full'); - rippleEl.css({ left: '50%', top: '50%' }); + elem.addClass('md-ripple-placed md-ripple-scaled').css({ left: '50%', top: '50%' }); + updateElement(elem); $timeout(function () { - if (rippleEl) { - rippleEl.removeClass('md-ripple-full'); - if (!rippleEl.hasClass('md-ripple-visible')) { - removeElement(rippleEl, 650); - rippleEl = null; - } - } - rippleEl && rippleEl.removeClass('md-ripple-full'); - if (rippleContainer && options.dimBackground) { - rippleContainer.removeClass('md-ripple-full'); - if (!rippleContainer.hasClass('md-ripple-visible')) rippleContainer.css({ backgroundColor: '' }); - } + state.animating = false; + updateElement(elem); }, 225, false); }, 0, false); - return rippleEl; - } - function onInput(ev) { - if (ev.eventType === Hammer.INPUT_START && ev.isFirst && rippleIsAllowed()) { - rippleEl = createRipple(ev.center.x, ev.center.y, true); - } else if (ev.eventType === Hammer.INPUT_END && ev.isFinal) { - if (rippleEl) { - rippleEl.removeClass('md-ripple-visible'); - removeElement(rippleEl, 650); - rippleEl = null; + return elem; + + /** + * + * @returns {*} + */ + function getRippleElement(css) { + var elem = angular.element('
'); + ripples.unshift(elem); + states.unshift({ animating: true }); + container.append(elem); + css && elem.css(css); + return elem; } - if (rippleContainer && options.dimBackground) { - rippleContainer.removeClass('md-ripple-visible'); - if (!rippleContainer.hasClass('md-ripple-full')) rippleContainer.css({ backgroundColor: '' }); + + /** + * + * @returns {*} + */ + function getRippleSize() { + var width = container.prop('offsetWidth'), + height = container.prop('offsetHeight'), + multiplier, size; + if (element.hasClass('md-menu-item')) { + size = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ); + } else { + multiplier = element.hasClass('md-fab') ? 1.1 : 0.8; + size = Math.max(width, height) * multiplier; } + return size; + } + + /** + * + * @returns {{backgroundColor: *, width: string, height: string, marginLeft: string, marginTop: string}} + */ + function getRippleCss(size, left, top) { + var css = { + backgroundColor: rgbaToRGB(color), + width: size + 'px', + height: size + 'px', + marginLeft: (size * -0.5) + 'px', + marginTop: (size * -0.5) + 'px' + }; + + contentParent && (top += contentParent.$element.prop('scrollTop')); + + if (options.center) { + css.left = css.top = '50%'; + } else { + var rect = node.getBoundingClientRect(); + css.left = Math.round((left - rect.left) / container.prop('offsetWidth') * 100) + '%'; + css.top = Math.round((top - rect.top) / container.prop('offsetHeight') * 100) + '%'; + } + + return css; + + /** + * + */ + function rgbaToRGB(color) { + return color.replace('rgba', 'rgb').replace(/,[^\)\,]+\)/, ')'); + } + } + + /** + * + */ + function getRippleContainer() { + if (rippleContainer) return rippleContainer; + var container = rippleContainer = angular.element('
'); + element.append(container); + return container; + } + } + + /** + * + */ + function onInput(ev) { + var ripple, index; + if (ev.eventType === Hammer.INPUT_START && ev.isFirst && isRippleAllowed()) { + ripple = createRipple(ev.center.x, ev.center.y); + isHeld = true; + } else if (ev.eventType === Hammer.INPUT_END && ev.isFinal) { + isHeld = false; + index = ripples.length - 1; + ripple = ripples[index]; + $timeout(function () { + updateElement(ripple); + }, 0, false); + } + + /** + * + */ + function isRippleAllowed() { + var parent = node.parentNode; + return !node.hasAttribute('disabled') && !(parent && parent.hasAttribute('disabled')); + } + } } } -} /** * noink/nobar/nostretch directive: make any element that has one of diff --git a/src/core/services/ripple/ripple.spec.js b/src/core/services/ripple/ripple.spec.js new file mode 100644 index 00000000000..fc1f25cedaf --- /dev/null +++ b/src/core/services/ripple/ripple.spec.js @@ -0,0 +1,31 @@ +describe('mdInkRipple diretive', function() { + + function simulateEventAt(centerX, eventType) { + return { + eventType: eventType, + center: { x: centerX }, + preventDefault: angular.noop, + srcEvent : { + stopPropagation : angular.noop + } + }; + } + + beforeEach(module('material.core')); + + it('should support custom colors via md-ink-ripple', inject(function ($timeout, $compile, $rootScope) { + var elem = $compile('
')($rootScope.$new()), + container, ripple; + + expect(elem.children('.md-ripple-container').length).toBe(0); + + elem.controller('mdInkRipple').createRipple(0, 0); + container = elem.children('.md-ripple-container'); + expect(container.length).toBe(1); + + ripple = container.children('.md-ripple'); + expect(ripple.length).toBe(1); + expect(ripple.css('backgroundColor')).toBe('rgb(187, 187, 187)'); + })); + +}); diff --git a/src/core/style/structure.scss b/src/core/style/structure.scss index b397a961f9c..242ff069e7d 100644 --- a/src/core/style/structure.scss +++ b/src/core/style/structure.scss @@ -248,12 +248,7 @@ input { &.md-ripple-scaled { transform: scale(1); } - &.md-ripple-full, &.md-ripple-visible { + &.md-ripple-active, &.md-ripple-full, &.md-ripple-visible { opacity: 0.20; } - &.md-ripple-held { - opacity: 0.15; - transform: scale(0.35); - transition: none; - } }