import { Directive } from 'vue'; import './index.less'; export interface RippleOptions { event: string; transition: number; } export interface RippleProto { background?: string; zIndex?: string; } export type EventType = Event & MouseEvent & TouchEvent; const options: RippleOptions = { event: 'mousedown', transition: 400, }; const RippleDirective: Directive & RippleProto = { beforeMount: (el: HTMLElement, binding) => { if (binding.value === false) return; const bg = el.getAttribute('ripple-background'); setProps(Object.keys(binding.modifiers), options); const background = bg || RippleDirective.background; const zIndex = RippleDirective.zIndex; el.addEventListener(options.event, (event: EventType) => { rippler({ event, el, background, zIndex, }); }); }, updated(el, binding) { if (!binding.value) { el?.clearRipple?.(); return; } const bg = el.getAttribute('ripple-background'); el?.setBackground?.(bg); }, }; function rippler({ event, el, zIndex, background, }: { event: EventType; el: HTMLElement } & RippleProto) { const targetBorder = parseInt(getComputedStyle(el).borderWidth.replace('px', '')); const clientX = event.clientX || event.touches[0].clientX; const clientY = event.clientY || event.touches[0].clientY; const rect = el.getBoundingClientRect(); const { left, top } = rect; const { offsetWidth: width, offsetHeight: height } = el; const { transition } = options; const dx = clientX - left; const dy = clientY - top; const maxX = Math.max(dx, width - dx); const maxY = Math.max(dy, height - dy); const style = window.getComputedStyle(el); const radius = Math.sqrt(maxX * maxX + maxY * maxY); const border = targetBorder > 0 ? targetBorder : 0; const ripple = document.createElement('div'); const rippleContainer = document.createElement('div'); // Styles for ripple ripple.className = 'ripple'; Object.assign(ripple.style ?? {}, { marginTop: '0px', marginLeft: '0px', width: '1px', height: '1px', transition: `all ${transition}ms cubic-bezier(0.4, 0, 0.2, 1)`, borderRadius: '50%', pointerEvents: 'none', position: 'relative', zIndex: zIndex ?? '9999', backgroundColor: background ?? 'rgba(0, 0, 0, 0.12)', }); // Styles for rippleContainer rippleContainer.className = 'ripple-container'; Object.assign(rippleContainer.style ?? {}, { position: 'absolute', left: `${0 - border}px`, top: `${0 - border}px`, height: '0', width: '0', pointerEvents: 'none', overflow: 'hidden', }); const storedTargetPosition = el.style.position.length > 0 ? el.style.position : getComputedStyle(el).position; if (storedTargetPosition !== 'relative') { el.style.position = 'relative'; } rippleContainer.appendChild(ripple); el.appendChild(rippleContainer); Object.assign(ripple.style, { marginTop: `${dy}px`, marginLeft: `${dx}px`, }); const { borderTopLeftRadius, borderTopRightRadius, borderBottomLeftRadius, borderBottomRightRadius, } = style; Object.assign(rippleContainer.style, { width: `${width}px`, height: `${height}px`, direction: 'ltr', borderTopLeftRadius, borderTopRightRadius, borderBottomLeftRadius, borderBottomRightRadius, }); setTimeout(() => { const wh = `${radius * 2}px`; Object.assign(ripple.style ?? {}, { width: wh, height: wh, marginLeft: `${dx - radius}px`, marginTop: `${dy - radius}px`, }); }, 0); function clearRipple() { setTimeout(() => { ripple.style.backgroundColor = 'rgba(0, 0, 0, 0)'; }, 250); setTimeout(() => { rippleContainer?.parentNode?.removeChild(rippleContainer); }, 850); el.removeEventListener('mouseup', clearRipple, false); el.removeEventListener('mouseleave', clearRipple, false); el.removeEventListener('dragstart', clearRipple, false); setTimeout(() => { let clearPosition = true; for (let i = 0; i < el.childNodes.length; i++) { if ((el.childNodes[i] as Recordable).className === 'ripple-container') { clearPosition = false; } } if (clearPosition) { el.style.position = storedTargetPosition !== 'static' ? storedTargetPosition : ''; } }, options.transition + 260); } if (event.type === 'mousedown') { el.addEventListener('mouseup', clearRipple, false); el.addEventListener('mouseleave', clearRipple, false); el.addEventListener('dragstart', clearRipple, false); } else { clearRipple(); } (el as Recordable).setBackground = (bgColor: string) => { if (!bgColor) { return; } ripple.style.backgroundColor = bgColor; }; } function setProps(modifiers: Hash, props: Recordable) { modifiers.forEach((item: Recordable) => { if (isNaN(Number(item))) props.event = item; else props.transition = item; }); } export default RippleDirective;