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
src/locales/lang/zh_CN/routes/demo/feat.ts
src/main.ts
... | ... | @@ -5,7 +5,7 @@ import router, { setupRouter } from '/@/router'; |
5 | 5 | import { setupStore } from '/@/store'; |
6 | 6 | import { setupAntd } from '/@/setup/ant-design-vue'; |
7 | 7 | import { setupErrorHandle } from '/@/setup/error-handle'; |
8 | -import { setupGlobDirectives } from '/@/setup/directives'; | |
8 | +import { setupGlobDirectives } from '/@/directives'; | |
9 | 9 | import { setupI18n } from '/@/setup/i18n'; |
10 | 10 | import { setupProdMockServer } from '../mock/_createProductionServer'; |
11 | 11 | import { setApp } from '/@/setup/App'; | ... | ... |
src/router/menus/modules/demo/feat.ts
... | ... | @@ -6,6 +6,9 @@ const menu: MenuModule = { |
6 | 6 | menu: { |
7 | 7 | name: t('routes.demo.feat.feat'), |
8 | 8 | path: '/feat', |
9 | + tag: { | |
10 | + dot: true, | |
11 | + }, | |
9 | 12 | |
10 | 13 | children: [ |
11 | 14 | { |
... | ... | @@ -45,6 +48,13 @@ const menu: MenuModule = { |
45 | 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 | 58 | path: 'full-screen', |
49 | 59 | name: t('routes.demo.feat.fullScreen'), |
50 | 60 | }, | ... | ... |
src/router/routes/modules/demo/feat.ts
... | ... | @@ -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 | 98 | path: 'full-screen', |
91 | 99 | name: 'FullScreenDemo', |
92 | 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> | ... | ... |