提交 2e79c9f3 编写于 作者: V vben

feat: add ripple directive

上级 aafbb052
## Wip
### ✨ Features
- 新增 `v-ripple`水波纹指令
### 🐛 Bug Fixes
- 修复混合模式下滚动条丢失问题
......
.ripple-container {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
overflow: hidden;
pointer-events: none;
}
.ripple-effect {
position: relative;
z-index: 9999;
width: 1px;
height: 1px;
margin-top: 0;
margin-left: 0;
pointer-events: none;
border-radius: 50%;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
import { 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 = 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
Object.assign(ripple.style ?? {}, {
className: 'ripple',
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
Object.assign(rippleContainer.style ?? {}, {
className: 'ripple-container',
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 any).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 any).setBackground = (bgColor: string) => {
if (!bgColor) {
return;
}
ripple.style.backgroundColor = bgColor;
};
}
function setProps(modifiers: { [key: string]: any }, props: Record<string, any>) {
modifiers.forEach((item: any) => {
if (isNaN(Number(item))) props.event = item;
else props.transition = item;
});
}
export default RippleDirective;
......@@ -9,6 +9,7 @@ export default {
copy: 'Clipboard',
msg: 'Message prompt',
watermark: 'Watermark',
ripple: 'Ripple',
fullScreen: 'Full Screen',
errorLog: 'Error Log',
tab: 'Tab with parameters',
......
......@@ -9,6 +9,7 @@ export default {
copy: '剪切板',
msg: '消息提示',
watermark: '水印',
ripple: '水波纹',
fullScreen: '全屏',
errorLog: '错误日志',
tab: 'Tab带参',
......
......@@ -5,7 +5,7 @@ import router, { setupRouter } from '/@/router';
import { setupStore } from '/@/store';
import { setupAntd } from '/@/setup/ant-design-vue';
import { setupErrorHandle } from '/@/setup/error-handle';
import { setupGlobDirectives } from '/@/setup/directives';
import { setupGlobDirectives } from '/@/directives';
import { setupI18n } from '/@/setup/i18n';
import { setupProdMockServer } from '../mock/_createProductionServer';
import { setApp } from '/@/setup/App';
......
......@@ -6,6 +6,9 @@ const menu: MenuModule = {
menu: {
name: t('routes.demo.feat.feat'),
path: '/feat',
tag: {
dot: true,
},
children: [
{
......@@ -44,6 +47,13 @@ const menu: MenuModule = {
path: 'watermark',
name: t('routes.demo.feat.watermark'),
},
{
path: 'ripple',
name: t('routes.demo.feat.ripple'),
tag: {
content: 'new',
},
},
{
path: 'full-screen',
name: t('routes.demo.feat.fullScreen'),
......
......@@ -86,6 +86,14 @@ const feat: AppRouteModule = {
title: t('routes.demo.feat.watermark'),
},
},
{
path: 'ripple',
name: 'RippleDemo',
component: () => import('/@/views/demo/feat/ripple/index.vue'),
meta: {
title: t('routes.demo.feat.ripple'),
},
},
{
path: 'full-screen',
name: 'FullScreenDemo',
......
<template>
<div class="p-4">
<div class="demo-box" v-ripple>content</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Alert } from 'ant-design-vue';
import RippleDirective from '/@/directives/ripple';
export default defineComponent({
components: { Alert },
directives: {
Ripple: RippleDirective,
},
setup() {
return {};
},
});
</script>
<style lang="less" scoped>
.demo-box {
display: flex;
width: 300px;
height: 300px;
font-size: 24px;
color: #fff;
background: #408ede;
border-radius: 10px;
justify-content: center;
align-items: center;
}
</style>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册