You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
181 lines
4.8 KiB
181 lines
4.8 KiB
import type { 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 = Number.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: Recordable, props: Recordable) { |
|
modifiers.forEach((item: Recordable) => { |
|
if (Number.isNaN(Number(item))) |
|
props.event = item |
|
else props.transition = item |
|
}) |
|
} |
|
|
|
export default RippleDirective
|
|
|