index.ts 4.9 KB

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