wip: a wave/ripple effect
This commit is contained in:
parent
7583883d64
commit
e2ce420764
30
src/resources/css/animations/ripple.css
Normal file
30
src/resources/css/animations/ripple.css
Normal file
@ -0,0 +1,30 @@
|
||||
:root {
|
||||
--ripple-color: #fff;
|
||||
--ripple-radius: 9999px;
|
||||
--ripple-duration: 600ms;
|
||||
--ripple-timing-function: linear;
|
||||
}
|
||||
|
||||
.ripple {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.ripple span {
|
||||
position: absolute;
|
||||
border-radius: var(--ripple-radius);
|
||||
opacity: 0.5;
|
||||
background: var(--ripple-color);
|
||||
transform: scale(0);
|
||||
animation: ripple var(--ripple-duration) var(--ripple-timing-function);
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
to {
|
||||
transform: scale(4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
96
src/resources/js/ripple.js
Normal file
96
src/resources/js/ripple.js
Normal file
@ -0,0 +1,96 @@
|
||||
import { getCustomColorFromModifiers, getCustomRadiusFromModifiers, willHaveAMouseUpEvent, toStyles } from './utils';
|
||||
|
||||
let rippleClass = 'ripple';
|
||||
let removeTimeout = 1000;
|
||||
|
||||
/**
|
||||
* Add a ripple effect to the element.
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
* @param {HTMLElement} el
|
||||
* @param {Array} modifiers
|
||||
*/
|
||||
export const addRipple = (event, el, modifiers) => {
|
||||
if (! willHaveAMouseUpEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ripple = document.createElement('span');
|
||||
rippleClass.split(' ').forEach(className => ripple.classList.add(className));
|
||||
|
||||
el.appendChild(ripple);
|
||||
|
||||
const size = ripple.offsetWidth,
|
||||
position = ripple.getBoundingClientRect(),
|
||||
innerRipple = document.createElement('span');
|
||||
|
||||
const x = event.pageX - position.left - (size / 2),
|
||||
y = event.pageY - position.top - (size / 2);
|
||||
|
||||
const style = {
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
};
|
||||
|
||||
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) {
|
||||
style['--ripple-color'] = color;
|
||||
}
|
||||
|
||||
const radius = getCustomRadiusFromModifiers(modifiers);
|
||||
if (radius) {
|
||||
style['--ripple-radius'] = radius;
|
||||
}
|
||||
|
||||
ripple.appendChild(innerRipple);
|
||||
innerRipple.setAttribute('style', toStyles(style));
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the ripple from the element.
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
export const removeRipple = 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(`.${rippleClass.replace(' ', '.')}`);
|
||||
|
||||
ripple && ripple.remove();
|
||||
}, removeTimeout);
|
||||
};
|
||||
|
||||
function Ripple(Alpine) {
|
||||
Alpine.directive('ripple', (el, { modifiers, expression }, { cleanup }) => {
|
||||
const clickHandler = event => addRipple(event, el, modifiers);
|
||||
const mouseUpHandler = () => removeRipple(el);
|
||||
|
||||
el.addEventListener('mousedown', clickHandler);
|
||||
el.addEventListener('mouseup', mouseUpHandler);
|
||||
|
||||
cleanup(() => {
|
||||
el.removeEventListener('mousedown', clickHandler);
|
||||
el.removeEventListener('mouseup', mouseUpHandler);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Ripple.configure = config => {
|
||||
if (config.hasOwnProperty('class') && typeof config.class === 'string') {
|
||||
rippleClass = config.class;
|
||||
}
|
||||
|
||||
if (config.hasOwnProperty('removeTimeout') && typeof config.removeTimeout === 'number') {
|
||||
removeTimeout = config.removeTimeout;
|
||||
}
|
||||
|
||||
return Ripple;
|
||||
};
|
||||
|
||||
export default Ripple;
|
67
src/resources/js/utils.js
Normal file
67
src/resources/js/utils.js
Normal file
@ -0,0 +1,67 @@
|
||||
export const getCustomColorFromModifiers = modifiers => {
|
||||
if (! modifiers.includes('color')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const nextModifier = modifiers[modifiers.indexOf('color') + 1] || 'invalid-color';
|
||||
if (nextModifier.indexOf('#') === 0 || nextModifier.indexOf('rgb') === 0) {
|
||||
return nextModifier;
|
||||
}
|
||||
|
||||
return nextModifier.indexOf('bg') === 0
|
||||
? nextModifier
|
||||
: `bg-${nextModifier}`;
|
||||
};
|
||||
|
||||
export const getCustomRadiusFromModifiers = modifiers => {
|
||||
if (! modifiers.includes('radius')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let nextModifier = modifiers[modifiers.indexOf('radius') + 1] || 'invalid-radius';
|
||||
|
||||
// _ allows us to use decimals, such as 0.5.
|
||||
nextModifier = nextModifier.replace('_', '.');
|
||||
|
||||
// Separate the numeric value from the unit in nextModifier.
|
||||
// Possible values for nextModifier: 50, 50.5, 50.5px, 50px, 50%, 50rem, 50em
|
||||
const numericValue = nextModifier.match(/^[0-9]+(\.[0-9]+)?/)[0];
|
||||
let unit = nextModifier.replace(numericValue, '');
|
||||
if (! unit) {
|
||||
unit = '%';
|
||||
}
|
||||
|
||||
return `${numericValue}${unit}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an object of style properties to a string of CSS.
|
||||
*
|
||||
* @param {Object} styles
|
||||
* @returns {string}
|
||||
*/
|
||||
export const toStyles = styles => Object.entries(styles).map(([key, value]) => `${formatStyleKey(key)}: ${value}`).join(';');
|
||||
|
||||
/**
|
||||
* Convert a style key to a CSS property.
|
||||
* Example: backgroundColor -> background-color
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {string}
|
||||
*/
|
||||
const formatStyleKey = key => key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||
|
||||
/**
|
||||
* Some events, such as a right click or ctrl + left click won't trigger a mouseup event,
|
||||
* so we need to prevent the ripple from being added in those cases.
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const willHaveAMouseUpEvent = event => {
|
||||
if (event.ctrlKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return event.button === 0 || event.button === 1;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user