import Util from './util' const Ripples = (($) => { /** * ------------------------------------------------------------------------ * Constants * ------------------------------------------------------------------------ */ const NAME = 'ripples' const DATA_KEY = `bmd.${NAME}` const JQUERY_NAME = `bmd${NAME.charAt(0).toUpperCase() + NAME.slice(1)}` const JQUERY_NO_CONFLICT = $.fn[JQUERY_NAME] const ClassName = { CONTAINER: 'ripple-container', DECORATOR: 'ripple-decorator' } const Selector = { CONTAINER: `.${ClassName.CONTAINER}`, DECORATOR: `.${ClassName.DECORATOR}` //, } const Default = { container: { template: `
` }, decorator: { template: `` }, trigger: { start: 'mousedown touchstart', end: 'mouseup mouseleave touchend' }, touchUserAgentRegex: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i, duration: 500 } /** * ------------------------------------------------------------------------ * Class Definition * ------------------------------------------------------------------------ */ class Ripples { constructor($element, config) { this.$element = $element // console.log(`Adding ripples to ${Util.describe(this.$element)}`) // eslint-disable-line no-console this.config = $.extend(true, {}, Default, config) // attach initial listener this.$element.on(this.config.trigger.start, (event) => { this._onStartRipple(event) }) } dispose() { this.$element.data(DATA_KEY, null) this.$element = null this.$container = null this.$decorator = null this.config = null } // ------------------------------------------------------------------------ // private _onStartRipple(event) { // Verify if the user is just touching on a device and return if so if (this._isTouch() && event.type === 'mousedown') { return } // Find or create the ripple container element this._findOrCreateContainer() // Get relY and relX positions of the container element let relY = this._getRelY(event) let relX = this._getRelX(event) // If relY and/or relX are false, return the event if (!relY && !relX) { return } // set the location and color each time (even if element is cached) this.$decorator.css({ left: relX, top: relY, 'background-color': this._getRipplesColor() }) // Make sure the ripple has the styles applied (ugly hack but it works) this._forceStyleApplication() // Turn on the ripple animation this.rippleOn() // Call the rippleEnd function when the transition 'on' ends setTimeout(() => { this.rippleEnd() }, this.config.duration) // Detect when the user leaves the element to cleanup if not already done? this.$element.on(this.config.trigger.end, () => { if (this.$decorator) { // guard against race condition/mouse attack this.$decorator.data('mousedown', 'off') if (this.$decorator.data('animating') === 'off') { this.rippleOut() } } }) } _findOrCreateContainer() { if (!this.$container || !this.$container.length > 0) { this.$element.append(this.config.container.template) this.$container = this.$element.find(Selector.CONTAINER) } // always add the rippleElement, it is always removed this.$container.append(this.config.decorator.template) this.$decorator = this.$container.find(Selector.DECORATOR) } // Make sure the ripple has the styles applied (ugly hack but it works) _forceStyleApplication() { return window.getComputedStyle(this.$decorator[0]).opacity } /** * Get the relX */ _getRelX(event) { let wrapperOffset = this.$container.offset() let result = null if (!this._isTouch()) { // Get the mouse position relative to the ripple wrapper result = event.pageX - wrapperOffset.left } else { // Make sure the user is using only one finger and then get the touch // position relative to the ripple wrapper event = event.originalEvent if (event.touches.length === 1) { result = event.touches[0].pageX - wrapperOffset.left } else { result = false } } return result } /** * Get the relY */ _getRelY(event) { let containerOffset = this.$container.offset() let result = null if (!this._isTouch()) { /** * Get the mouse position relative to the ripple wrapper */ result = event.pageY - containerOffset.top } else { /** * Make sure the user is using only one finger and then get the touch * position relative to the ripple wrapper */ event = event.originalEvent if (event.touches.length === 1) { result = event.touches[0].pageY - containerOffset.top } else { result = false } } return result } /** * Get the ripple color */ _getRipplesColor() { let color = this.$element.data('ripple-color') ? this.$element.data('ripple-color') : window.getComputedStyle(this.$element[0]).color return color } /** * Verify if the client is using a mobile device */ _isTouch() { return this.config.touchUserAgentRegex.test(navigator.userAgent) } /** * End the animation of the ripple */ rippleEnd() { if (this.$decorator) { // guard against race condition/mouse attack this.$decorator.data('animating', 'off') if (this.$decorator.data('mousedown') === 'off') { this.rippleOut(this.$decorator) } } } /** * Turn off the ripple effect */ rippleOut() { this.$decorator.off() if (Util.transitionEndSupported()) { this.$decorator.addClass('ripple-out') } else { this.$decorator.animate({opacity: 0}, 100, () => { this.$decorator.trigger('transitionend') }) } this.$decorator.on(Util.transitionEndSelector(), () => { if (this.$decorator) { this.$decorator.remove() this.$decorator = null } }) } /** * Turn on the ripple effect */ rippleOn() { let size = this._getNewSize() if (Util.transitionEndSupported()) { this.$decorator .css({ '-ms-transform': `scale(${size})`, '-moz-transform': `scale(${size})`, '-webkit-transform': `scale(${size})`, transform: `scale(${size})` }) .addClass('ripple-on') .data('animating', 'on') .data('mousedown', 'on') } else { this.$decorator.animate({ width: Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * 2, height: Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * 2, 'margin-left': Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * (-1), 'margin-top': Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * (-1), opacity: 0.2 }, this.config.duration, () => { this.$decorator.trigger('transitionend') }) } } /** * Get the new size based on the element height/width and the ripple width */ _getNewSize() { return (Math.max(this.$element.outerWidth(), this.$element.outerHeight()) / this.$decorator.outerWidth()) * 2.5 } // ------------------------------------------------------------------------ // static static _jQueryInterface(config) { return this.each(function () { let $element = $(this) let data = $element.data(DATA_KEY) if (!data) { data = new Ripples($element, config) $element.data(DATA_KEY, data) } }) } } /** * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ */ $.fn[JQUERY_NAME] = Ripples._jQueryInterface $.fn[JQUERY_NAME].Constructor = Ripples $.fn[JQUERY_NAME].noConflict = () => { $.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT return Ripples._jQueryInterface } return Ripples })(jQuery) export default Ripples