Commit 2e79c9f37adda4003e6b054561b26da69a762673
1 parent
aafbb052
feat: add ripple directive
Showing
13 changed files
with
270 additions
and
1 deletions
CHANGELOG.zh_CN.md
src/setup/directives/index.ts renamed to src/directives/index.ts
src/setup/directives/loading.ts renamed to src/directives/loading.ts
src/setup/directives/permission.ts renamed to src/directives/permission.ts
src/setup/directives/repeatClick.ts renamed to src/directives/repeatClick.ts
src/directives/ripple/index.less
0 → 100644
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
0 → 100644
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
@@ -9,6 +9,7 @@ export default { | @@ -9,6 +9,7 @@ export default { | ||
9 | copy: 'Clipboard', | 9 | copy: 'Clipboard', |
10 | msg: 'Message prompt', | 10 | msg: 'Message prompt', |
11 | watermark: 'Watermark', | 11 | watermark: 'Watermark', |
12 | + ripple: 'Ripple', | ||
12 | fullScreen: 'Full Screen', | 13 | fullScreen: 'Full Screen', |
13 | errorLog: 'Error Log', | 14 | errorLog: 'Error Log', |
14 | tab: 'Tab with parameters', | 15 | tab: 'Tab with parameters', |
src/locales/lang/zh_CN/routes/demo/feat.ts
@@ -9,6 +9,7 @@ export default { | @@ -9,6 +9,7 @@ export default { | ||
9 | copy: '剪切板', | 9 | copy: '剪切板', |
10 | msg: '消息提示', | 10 | msg: '消息提示', |
11 | watermark: '水印', | 11 | watermark: '水印', |
12 | + ripple: '水波纹', | ||
12 | fullScreen: '全屏', | 13 | fullScreen: '全屏', |
13 | errorLog: '错误日志', | 14 | errorLog: '错误日志', |
14 | tab: 'Tab带参', | 15 | tab: 'Tab带参', |
src/main.ts
@@ -5,7 +5,7 @@ import router, { setupRouter } from '/@/router'; | @@ -5,7 +5,7 @@ import router, { setupRouter } from '/@/router'; | ||
5 | import { setupStore } from '/@/store'; | 5 | import { setupStore } from '/@/store'; |
6 | import { setupAntd } from '/@/setup/ant-design-vue'; | 6 | import { setupAntd } from '/@/setup/ant-design-vue'; |
7 | import { setupErrorHandle } from '/@/setup/error-handle'; | 7 | import { setupErrorHandle } from '/@/setup/error-handle'; |
8 | -import { setupGlobDirectives } from '/@/setup/directives'; | 8 | +import { setupGlobDirectives } from '/@/directives'; |
9 | import { setupI18n } from '/@/setup/i18n'; | 9 | import { setupI18n } from '/@/setup/i18n'; |
10 | import { setupProdMockServer } from '../mock/_createProductionServer'; | 10 | import { setupProdMockServer } from '../mock/_createProductionServer'; |
11 | import { setApp } from '/@/setup/App'; | 11 | import { setApp } from '/@/setup/App'; |
src/router/menus/modules/demo/feat.ts
@@ -6,6 +6,9 @@ const menu: MenuModule = { | @@ -6,6 +6,9 @@ const menu: MenuModule = { | ||
6 | menu: { | 6 | menu: { |
7 | name: t('routes.demo.feat.feat'), | 7 | name: t('routes.demo.feat.feat'), |
8 | path: '/feat', | 8 | path: '/feat', |
9 | + tag: { | ||
10 | + dot: true, | ||
11 | + }, | ||
9 | 12 | ||
10 | children: [ | 13 | children: [ |
11 | { | 14 | { |
@@ -45,6 +48,13 @@ const menu: MenuModule = { | @@ -45,6 +48,13 @@ const menu: MenuModule = { | ||
45 | name: t('routes.demo.feat.watermark'), | 48 | name: t('routes.demo.feat.watermark'), |
46 | }, | 49 | }, |
47 | { | 50 | { |
51 | + path: 'ripple', | ||
52 | + name: t('routes.demo.feat.ripple'), | ||
53 | + tag: { | ||
54 | + content: 'new', | ||
55 | + }, | ||
56 | + }, | ||
57 | + { | ||
48 | path: 'full-screen', | 58 | path: 'full-screen', |
49 | name: t('routes.demo.feat.fullScreen'), | 59 | name: t('routes.demo.feat.fullScreen'), |
50 | }, | 60 | }, |
src/router/routes/modules/demo/feat.ts
@@ -87,6 +87,14 @@ const feat: AppRouteModule = { | @@ -87,6 +87,14 @@ const feat: AppRouteModule = { | ||
87 | }, | 87 | }, |
88 | }, | 88 | }, |
89 | { | 89 | { |
90 | + path: 'ripple', | ||
91 | + name: 'RippleDemo', | ||
92 | + component: () => import('/@/views/demo/feat/ripple/index.vue'), | ||
93 | + meta: { | ||
94 | + title: t('routes.demo.feat.ripple'), | ||
95 | + }, | ||
96 | + }, | ||
97 | + { | ||
90 | path: 'full-screen', | 98 | path: 'full-screen', |
91 | name: 'FullScreenDemo', | 99 | name: 'FullScreenDemo', |
92 | component: () => import('/@/views/demo/feat/full-screen/index.vue'), | 100 | component: () => import('/@/views/demo/feat/full-screen/index.vue'), |
src/views/demo/feat/ripple/index.vue
0 → 100644
1 | +<template> | ||
2 | + <div class="p-4"> | ||
3 | + <div class="demo-box" v-ripple>content</div> | ||
4 | + </div> | ||
5 | +</template> | ||
6 | +<script lang="ts"> | ||
7 | + import { defineComponent } from 'vue'; | ||
8 | + import { Alert } from 'ant-design-vue'; | ||
9 | + import RippleDirective from '/@/directives/ripple'; | ||
10 | + export default defineComponent({ | ||
11 | + components: { Alert }, | ||
12 | + directives: { | ||
13 | + Ripple: RippleDirective, | ||
14 | + }, | ||
15 | + setup() { | ||
16 | + return {}; | ||
17 | + }, | ||
18 | + }); | ||
19 | +</script> | ||
20 | + | ||
21 | +<style lang="less" scoped> | ||
22 | + .demo-box { | ||
23 | + display: flex; | ||
24 | + width: 300px; | ||
25 | + height: 300px; | ||
26 | + font-size: 24px; | ||
27 | + color: #fff; | ||
28 | + background: #408ede; | ||
29 | + border-radius: 10px; | ||
30 | + justify-content: center; | ||
31 | + align-items: center; | ||
32 | + } | ||
33 | +</style> |