index.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { 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) => {
  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({
  42. event,
  43. el,
  44. zIndex,
  45. background,
  46. }: { event: EventType; el: HTMLElement } & RippleProto) {
  47. const targetBorder = parseInt(getComputedStyle(el).borderWidth.replace('px', ''));
  48. const clientX = event.clientX || event.touches[0].clientX;
  49. const clientY = event.clientY || event.touches[0].clientY;
  50. const rect = el.getBoundingClientRect();
  51. const { left, top } = rect;
  52. const { offsetWidth: width, offsetHeight: height } = el;
  53. const { transition } = options;
  54. const dx = clientX - left;
  55. const dy = clientY - top;
  56. const maxX = Math.max(dx, width - dx);
  57. const maxY = Math.max(dy, height - dy);
  58. const style = window.getComputedStyle(el);
  59. const radius = Math.sqrt(maxX * maxX + maxY * maxY);
  60. const border = targetBorder > 0 ? targetBorder : 0;
  61. const ripple = document.createElement('div');
  62. const rippleContainer = document.createElement('div');
  63. // Styles for ripple
  64. ripple.className = 'ripple';
  65. Object.assign(ripple.style ?? {}, {
  66. marginTop: '0px',
  67. marginLeft: '0px',
  68. width: '1px',
  69. height: '1px',
  70. transition: `all ${transition}ms cubic-bezier(0.4, 0, 0.2, 1)`,
  71. borderRadius: '50%',
  72. pointerEvents: 'none',
  73. position: 'relative',
  74. zIndex: zIndex ?? '9999',
  75. backgroundColor: background ?? 'rgba(0, 0, 0, 0.12)',
  76. });
  77. // Styles for rippleContainer
  78. rippleContainer.className = 'ripple-container';
  79. Object.assign(rippleContainer.style ?? {}, {
  80. position: 'absolute',
  81. left: `${0 - border}px`,
  82. top: `${0 - border}px`,
  83. height: '0',
  84. width: '0',
  85. pointerEvents: 'none',
  86. overflow: 'hidden',
  87. });
  88. const storedTargetPosition =
  89. el.style.position.length > 0 ? el.style.position : getComputedStyle(el).position;
  90. if (storedTargetPosition !== 'relative') {
  91. el.style.position = 'relative';
  92. }
  93. rippleContainer.appendChild(ripple);
  94. el.appendChild(rippleContainer);
  95. Object.assign(ripple.style, {
  96. marginTop: `${dy}px`,
  97. marginLeft: `${dx}px`,
  98. });
  99. const {
  100. borderTopLeftRadius,
  101. borderTopRightRadius,
  102. borderBottomLeftRadius,
  103. borderBottomRightRadius,
  104. } = style;
  105. Object.assign(rippleContainer.style, {
  106. width: `${width}px`,
  107. height: `${height}px`,
  108. direction: 'ltr',
  109. borderTopLeftRadius,
  110. borderTopRightRadius,
  111. borderBottomLeftRadius,
  112. borderBottomRightRadius,
  113. });
  114. setTimeout(() => {
  115. const wh = `${radius * 2}px`;
  116. Object.assign(ripple.style ?? {}, {
  117. width: wh,
  118. height: wh,
  119. marginLeft: `${dx - radius}px`,
  120. marginTop: `${dy - radius}px`,
  121. });
  122. }, 0);
  123. function clearRipple() {
  124. setTimeout(() => {
  125. ripple.style.backgroundColor = 'rgba(0, 0, 0, 0)';
  126. }, 250);
  127. setTimeout(() => {
  128. rippleContainer?.parentNode?.removeChild(rippleContainer);
  129. }, 850);
  130. el.removeEventListener('mouseup', clearRipple, false);
  131. el.removeEventListener('mouseleave', clearRipple, false);
  132. el.removeEventListener('dragstart', clearRipple, false);
  133. setTimeout(() => {
  134. let clearPosition = true;
  135. for (let i = 0; i < el.childNodes.length; i++) {
  136. if ((el.childNodes[i] as Recordable).className === 'ripple-container') {
  137. clearPosition = false;
  138. }
  139. }
  140. if (clearPosition) {
  141. el.style.position = storedTargetPosition !== 'static' ? storedTargetPosition : '';
  142. }
  143. }, options.transition + 260);
  144. }
  145. if (event.type === 'mousedown') {
  146. el.addEventListener('mouseup', clearRipple, false);
  147. el.addEventListener('mouseleave', clearRipple, false);
  148. el.addEventListener('dragstart', clearRipple, false);
  149. } else {
  150. clearRipple();
  151. }
  152. (el as Recordable).setBackground = (bgColor: string) => {
  153. if (!bgColor) {
  154. return;
  155. }
  156. ripple.style.backgroundColor = bgColor;
  157. };
  158. }
  159. function setProps(modifiers: Hash<any>, props: Recordable) {
  160. modifiers.forEach((item: Recordable) => {
  161. if (isNaN(Number(item))) props.event = item;
  162. else props.transition = item;
  163. });
  164. }
  165. export default RippleDirective;