mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-27 14:13:40 +08:00
feat: add ripple directive
This commit is contained in:
21
src/directives/ripple/index.less
Normal file
21
src/directives/ripple/index.less
Normal file
@@ -0,0 +1,21 @@
|
||||
.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);
|
||||
}
|
191
src/directives/ripple/index.ts
Normal file
191
src/directives/ripple/index.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
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',
|
||||
|
33
src/views/demo/feat/ripple/index.vue
Normal file
33
src/views/demo/feat/ripple/index.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<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>
|
Reference in New Issue
Block a user