@wilkr/alpine-ripple
Version:
Ripple effect (materialize) for Alpine.js.
174 lines (147 loc) • 5.7 kB
JavaScript
import {
addConfigClassToElement,
configClassToSelector,
getAttributeThatStartsWith,
getCustomColorFromModifiers,
getCustomRadiusFromModifiers,
isEnterOrSpace,
startRipple,
toStyles,
willHaveAMouseUpEvent,
} from '../utils';
import { addRippleFocus, removeRippleFocus } from './focus';
/**
* Configuration options for the ripple click directive.
*
* @type {{removeTimeout: number, rippleClass: string}}
*/
let config = {
rippleClass: 'ripple',
removeTimeout: 1000,
focusedClass: 'ripple-focus-active', // So we can check if the element is focused by our directive.
};
/**
* Check if the element has the focused class.
*
* @param {HTMLElement} el
* @returns {boolean}
*/
const hasRippleFocus = el => config.focusedClass.split(' ').every(className => el.classList.contains(className));
/**
* Add a ripple effect to the element.
*
* @param {MouseEvent|KeyboardEvent} event
* @param {HTMLElement} el
* @param {Array} modifiers
*/
export const addRipple = (event, el, modifiers = []) => {
if (! willHaveAMouseUpEvent(event) && ! isEnterOrSpace(event)) {
return;
}
const styles = startRipple(event, el);
const ripple = document.createElement('span');
addConfigClassToElement(ripple, config.rippleClass);
el.appendChild(ripple);
const innerRipple = document.createElement('span');
const color = getCustomColorFromModifiers(modifiers);
if (color.indexOf('bg-') === 0) {
// Prefix with '!' for !important (requires Tailwind).
innerRipple.classList.add(`!${color}`);
} else if (color.indexOf('#') === 0 || color.indexOf('rgb') === 0) {
styles['--ripple-color'] = color;
}
const radius = getCustomRadiusFromModifiers(modifiers);
if (radius) {
styles['--ripple-radius'] = radius;
}
ripple.appendChild(innerRipple);
innerRipple.setAttribute('style', toStyles(styles));
};
/**
* Remove the ripple effect from the element.
*
* @param {HTMLElement} el
* @param {boolean} alsoRemoveFocus
*/
export const removeRipple = (el, alsoRemoveFocus = false) => {
alsoRemoveFocus && removeRippleFocus(el);
setTimeout(() => {
// We are only removing the first instance to prevent ripples from subsequent clicks
// being removed too quickly before the ripple effect can properly be seen.
const ripple = el.querySelector(configClassToSelector(config.rippleClass));
ripple && ripple.remove();
}, config.removeTimeout);
};
/**
* Show a ripple effect when the user presses the enter or space key.
*
* @param {KeyboardEvent} event
* @param {HTMLElement} el
* @param {array} modifiers
*/
export const handleRippleKeydown = (event, el, modifiers) => {
if (! isEnterOrSpace(event)) {
return;
}
const originallyHadFocus = hasRippleFocus(el);
addRipple(event, el, modifiers);
removeRippleFocus(el);
if (originallyHadFocus) {
addFocusBack(event, el);
}
};
/**
* Add a focus ripple effect back to the element after the ripple effect has been removed.
*
* @param {Event} event
* @param {HTMLElement} el
*/
const addFocusBack = (event, el) => {
const hasRippleClick = el.contains(el.querySelector(configClassToSelector(config.rippleClass)));
if (hasRippleClick) {
setTimeout(() => addFocusBack(event, el), config.removeTimeout);
return;
}
// We need to get the modifiers, if any, from the x-ripple-focus directive.
const directive = getAttributeThatStartsWith(el, 'x-ripple-focus');
const focusModifiers = directive ? directive.name.split('.').slice(1) : [];
addRippleFocus(event, el, focusModifiers);
};
/**
* Define an Alpine directive that adds a ripple click effect to a given element.
*
* @param {Object} Alpine
* @param {Object} rippleConfig
*/
export default (Alpine, rippleConfig) => {
config = { ...config, ...rippleConfig };
Alpine.directive('ripple', (el, { modifiers, expression }, { cleanup }) => {
const clickHandler = event => addRipple(event, el, modifiers);
const mouseUpHandler = () => removeRipple(el, true);
const keydownHandler = event => handleRippleKeydown(event, el, modifiers);
const keyupHandler = event => isEnterOrSpace(event) && removeRipple(el, false);
const passiveOptions = { passive: true };
el.addEventListener('mousedown', clickHandler);
el.addEventListener('mouseup', mouseUpHandler);
el.addEventListener('mouseleave', mouseUpHandler);
el.addEventListener('contextmenu', mouseUpHandler);
el.addEventListener('touchstart', clickHandler, passiveOptions);
el.addEventListener('touchend', mouseUpHandler);
el.addEventListener('touchmove', mouseUpHandler, passiveOptions);
el.addEventListener('dragleave', mouseUpHandler);
el.addEventListener('keydown', keydownHandler);
el.addEventListener('keyup', keyupHandler);
cleanup(() => {
el.removeEventListener('mousedown', clickHandler);
el.removeEventListener('mouseup', mouseUpHandler);
el.removeEventListener('mouseleave', mouseUpHandler);
el.removeEventListener('contextmenu', mouseUpHandler);
el.removeEventListener('touchstart', clickHandler, passiveOptions);
el.removeEventListener('touchend', mouseUpHandler);
el.removeEventListener('touchmove', mouseUpHandler, passiveOptions);
el.removeEventListener('dragleave', mouseUpHandler);
el.removeEventListener('keydown', keydownHandler);
el.removeEventListener('keyup', keyupHandler);
});
});
};