Skip to content

Commit 2e79c9f

Browse files
committed
feat: add ripple directive
1 parent aafbb05 commit 2e79c9f

File tree

13 files changed

+270
-1
lines changed

13 files changed

+270
-1
lines changed

CHANGELOG.zh_CN.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Wip
22

3+
### ✨ Features
4+
5+
- 新增 `v-ripple`水波纹指令
6+
37
### 🐛 Bug Fixes
48

59
- 修复混合模式下滚动条丢失问题

src/directives/ripple/index.less

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.ripple-container {
2+
position: absolute;
3+
top: 0;
4+
left: 0;
5+
width: 0;
6+
height: 0;
7+
overflow: hidden;
8+
pointer-events: none;
9+
}
10+
11+
.ripple-effect {
12+
position: relative;
13+
z-index: 9999;
14+
width: 1px;
15+
height: 1px;
16+
margin-top: 0;
17+
margin-left: 0;
18+
pointer-events: none;
19+
border-radius: 50%;
20+
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
21+
}

src/directives/ripple/index.ts

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

src/locales/lang/en/routes/demo/feat.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default {
99
copy: 'Clipboard',
1010
msg: 'Message prompt',
1111
watermark: 'Watermark',
12+
ripple: 'Ripple',
1213
fullScreen: 'Full Screen',
1314
errorLog: 'Error Log',
1415
tab: 'Tab with parameters',

src/locales/lang/zh_CN/routes/demo/feat.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default {
99
copy: '剪切板',
1010
msg: '消息提示',
1111
watermark: '水印',
12+
ripple: '水波纹',
1213
fullScreen: '全屏',
1314
errorLog: '错误日志',
1415
tab: 'Tab带参',

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import router, { setupRouter } from '/@/router';
55
import { setupStore } from '/@/store';
66
import { setupAntd } from '/@/setup/ant-design-vue';
77
import { setupErrorHandle } from '/@/setup/error-handle';
8-
import { setupGlobDirectives } from '/@/setup/directives';
8+
import { setupGlobDirectives } from '/@/directives';
99
import { setupI18n } from '/@/setup/i18n';
1010
import { setupProdMockServer } from '../mock/_createProductionServer';
1111
import { setApp } from '/@/setup/App';

0 commit comments

Comments
 (0)