vben
2023-04-05 8e5a6b7ce547ba8edb1d767bb4d820f3b66ff95a
提交 | 用户 | age
236575 1 import type { Directive } from 'vue';
2e79c9 2 import './index.less';
8e5a6b 3
2e79c9 4 export interface RippleOptions {
V 5   event: string;
6   transition: number;
7 }
8
9 export interface RippleProto {
10   background?: string;
11   zIndex?: string;
12 }
13
14 export type EventType = Event & MouseEvent & TouchEvent;
15
16 const options: RippleOptions = {
17   event: 'mousedown',
18   transition: 400,
19 };
20
21 const RippleDirective: Directive & RippleProto = {
22   beforeMount: (el: HTMLElement, binding) => {
23     if (binding.value === false) return;
24
25     const bg = el.getAttribute('ripple-background');
26     setProps(Object.keys(binding.modifiers), options);
27
28     const background = bg || RippleDirective.background;
29     const zIndex = RippleDirective.zIndex;
30
31     el.addEventListener(options.event, (event: EventType) => {
32       rippler({
33         event,
34         el,
35         background,
36         zIndex,
37       });
38     });
39   },
40   updated(el, binding) {
41     if (!binding.value) {
42       el?.clearRipple?.();
43       return;
44     }
45     const bg = el.getAttribute('ripple-background');
46     el?.setBackground?.(bg);
47   },
48 };
49
50 function rippler({
51   event,
52   el,
53   zIndex,
54   background,
55 }: { event: EventType; el: HTMLElement } & RippleProto) {
56   const targetBorder = parseInt(getComputedStyle(el).borderWidth.replace('px', ''));
57   const clientX = event.clientX || event.touches[0].clientX;
58   const clientY = event.clientY || event.touches[0].clientY;
59
60   const rect = el.getBoundingClientRect();
61   const { left, top } = rect;
62   const { offsetWidth: width, offsetHeight: height } = el;
63   const { transition } = options;
64   const dx = clientX - left;
65   const dy = clientY - top;
66   const maxX = Math.max(dx, width - dx);
67   const maxY = Math.max(dy, height - dy);
68   const style = window.getComputedStyle(el);
69   const radius = Math.sqrt(maxX * maxX + maxY * maxY);
70   const border = targetBorder > 0 ? targetBorder : 0;
71
72   const ripple = document.createElement('div');
73   const rippleContainer = document.createElement('div');
74
75   // Styles for ripple
220162 76   ripple.className = 'ripple';
2e79c9 77
V 78   Object.assign(ripple.style ?? {}, {
79     marginTop: '0px',
80     marginLeft: '0px',
81     width: '1px',
82     height: '1px',
83     transition: `all ${transition}ms cubic-bezier(0.4, 0, 0.2, 1)`,
84     borderRadius: '50%',
85     pointerEvents: 'none',
86     position: 'relative',
87     zIndex: zIndex ?? '9999',
88     backgroundColor: background ?? 'rgba(0, 0, 0, 0.12)',
89   });
90
91   // Styles for rippleContainer
220162 92   rippleContainer.className = 'ripple-container';
2e79c9 93   Object.assign(rippleContainer.style ?? {}, {
V 94     position: 'absolute',
95     left: `${0 - border}px`,
96     top: `${0 - border}px`,
97     height: '0',
98     width: '0',
99     pointerEvents: 'none',
100     overflow: 'hidden',
101   });
102
103   const storedTargetPosition =
104     el.style.position.length > 0 ? el.style.position : getComputedStyle(el).position;
105
106   if (storedTargetPosition !== 'relative') {
107     el.style.position = 'relative';
108   }
109
110   rippleContainer.appendChild(ripple);
111   el.appendChild(rippleContainer);
112
113   Object.assign(ripple.style, {
114     marginTop: `${dy}px`,
115     marginLeft: `${dx}px`,
116   });
117
118   const {
119     borderTopLeftRadius,
120     borderTopRightRadius,
121     borderBottomLeftRadius,
122     borderBottomRightRadius,
123   } = style;
124   Object.assign(rippleContainer.style, {
125     width: `${width}px`,
126     height: `${height}px`,
127     direction: 'ltr',
128     borderTopLeftRadius,
129     borderTopRightRadius,
130     borderBottomLeftRadius,
131     borderBottomRightRadius,
132   });
133
134   setTimeout(() => {
135     const wh = `${radius * 2}px`;
136     Object.assign(ripple.style ?? {}, {
137       width: wh,
138       height: wh,
139       marginLeft: `${dx - radius}px`,
140       marginTop: `${dy - radius}px`,
141     });
142   }, 0);
143
144   function clearRipple() {
145     setTimeout(() => {
146       ripple.style.backgroundColor = 'rgba(0, 0, 0, 0)';
147     }, 250);
148
149     setTimeout(() => {
150       rippleContainer?.parentNode?.removeChild(rippleContainer);
151     }, 850);
152     el.removeEventListener('mouseup', clearRipple, false);
153     el.removeEventListener('mouseleave', clearRipple, false);
154     el.removeEventListener('dragstart', clearRipple, false);
155     setTimeout(() => {
156       let clearPosition = true;
157       for (let i = 0; i < el.childNodes.length; i++) {
4ff1c4 158         if ((el.childNodes[i] as Recordable).className === 'ripple-container') {
2e79c9 159           clearPosition = false;
V 160         }
161       }
162
163       if (clearPosition) {
164         el.style.position = storedTargetPosition !== 'static' ? storedTargetPosition : '';
165       }
166     }, options.transition + 260);
167   }
168
169   if (event.type === 'mousedown') {
170     el.addEventListener('mouseup', clearRipple, false);
171     el.addEventListener('mouseleave', clearRipple, false);
172     el.addEventListener('dragstart', clearRipple, false);
173   } else {
174     clearRipple();
175   }
176
4ff1c4 177   (el as Recordable).setBackground = (bgColor: string) => {
2e79c9 178     if (!bgColor) {
V 179       return;
180     }
181     ripple.style.backgroundColor = bgColor;
182   };
183 }
184
a98835 185 function setProps(modifiers: Recordable, props: Recordable) {
4ff1c4 186   modifiers.forEach((item: Recordable) => {
2e79c9 187     if (isNaN(Number(item))) props.event = item;
V 188     else props.transition = item;
189   });
190 }
191
192 export default RippleDirective;