|
| 1 | +<template> |
| 2 | + <div v-if="show && !disabled" ref="divRef" style="position: absolute; left: 0; top: 0"> |
| 3 | + <Transition |
| 4 | + appear |
| 5 | + name="ant-wave-motion" |
| 6 | + appearFromClass="ant-wave-motion-appear" |
| 7 | + appearActiveClass="ant-wave-motion-appear" |
| 8 | + appearToClass="ant-wave-motion-appear ant-wave-motion-appear-active" |
| 9 | + > |
| 10 | + <div |
| 11 | + v-if="show" |
| 12 | + :style="waveStyle" |
| 13 | + class="ant-wave-motion" |
| 14 | + @transitionend="onTransitionend" |
| 15 | + /> |
| 16 | + </Transition> |
| 17 | + </div> |
| 18 | +</template> |
| 19 | +<script setup lang="ts"> |
| 20 | +import wrapperRaf from '@/utils/raf' |
| 21 | +import { |
| 22 | + computed, |
| 23 | + nextTick, |
| 24 | + onBeforeUnmount, |
| 25 | + onMounted, |
| 26 | + ref, |
| 27 | + shallowRef, |
| 28 | + Transition, |
| 29 | + watch, |
| 30 | +} from 'vue' |
| 31 | +import { getTargetWaveColor } from './util' |
| 32 | +import isVisible from '@/utils/isVisible' |
| 33 | + |
| 34 | +const props = defineProps<{ |
| 35 | + disabled?: boolean |
| 36 | + target: HTMLElement |
| 37 | +}>() |
| 38 | + |
| 39 | +const divRef = shallowRef<HTMLDivElement | null>(null) |
| 40 | + |
| 41 | +const show = defineModel<boolean>('show') |
| 42 | + |
| 43 | +const color = ref<string | null>(null) |
| 44 | +const borderRadius = ref<number[]>([]) |
| 45 | +const left = ref(0) |
| 46 | +const top = ref(0) |
| 47 | +const width = ref(0) |
| 48 | +const height = ref(0) |
| 49 | + |
| 50 | +function validateNum(value: number) { |
| 51 | + return Number.isNaN(value) ? 0 : value |
| 52 | +} |
| 53 | +function syncPos() { |
| 54 | + const { target } = props |
| 55 | + if (!target) { |
| 56 | + return |
| 57 | + } |
| 58 | + const nodeStyle = getComputedStyle(target) |
| 59 | + |
| 60 | + // Get wave color from target |
| 61 | + color.value = getTargetWaveColor(target) |
| 62 | + |
| 63 | + const isStatic = nodeStyle.position === 'static' |
| 64 | + |
| 65 | + // Rect |
| 66 | + const { borderLeftWidth, borderTopWidth } = nodeStyle |
| 67 | + left.value = isStatic ? target.offsetLeft : validateNum(-parseFloat(borderLeftWidth)) |
| 68 | + top.value = isStatic ? target.offsetTop : validateNum(-parseFloat(borderTopWidth)) |
| 69 | + width.value = target.offsetWidth |
| 70 | + height.value = target.offsetHeight |
| 71 | + |
| 72 | + // Get border radius |
| 73 | + const { |
| 74 | + borderTopLeftRadius, |
| 75 | + borderTopRightRadius, |
| 76 | + borderBottomLeftRadius, |
| 77 | + borderBottomRightRadius, |
| 78 | + } = nodeStyle |
| 79 | + |
| 80 | + borderRadius.value = [ |
| 81 | + borderTopLeftRadius, |
| 82 | + borderTopRightRadius, |
| 83 | + borderBottomRightRadius, |
| 84 | + borderBottomLeftRadius, |
| 85 | + ].map(radius => validateNum(parseFloat(radius))) |
| 86 | +} |
| 87 | +// Add resize observer to follow size |
| 88 | +let resizeObserver: ResizeObserver |
| 89 | +let rafId: number |
| 90 | +let timeoutId: any |
| 91 | +let onClick: (e: MouseEvent) => void |
| 92 | +const clear = () => { |
| 93 | + clearTimeout(timeoutId) |
| 94 | + wrapperRaf.cancel(rafId) |
| 95 | + resizeObserver?.disconnect() |
| 96 | + const { target } = props |
| 97 | + target?.removeEventListener('click', onClick, true) |
| 98 | +} |
| 99 | + |
| 100 | +const init = () => { |
| 101 | + clear() |
| 102 | + const { target } = props |
| 103 | + if (target) { |
| 104 | + target?.removeEventListener('click', onClick, true) |
| 105 | + if (!target || target.nodeType !== 1) { |
| 106 | + return |
| 107 | + } |
| 108 | + // Click handler |
| 109 | + onClick = (e: MouseEvent) => { |
| 110 | + // Fix radio button click twice |
| 111 | + if ( |
| 112 | + (e.target as HTMLElement).tagName === 'INPUT' || |
| 113 | + !isVisible(e.target as HTMLElement) || |
| 114 | + // No need wave |
| 115 | + !target.getAttribute || |
| 116 | + target.getAttribute('disabled') || |
| 117 | + (target as HTMLInputElement).disabled || |
| 118 | + target.className.includes('disabled') || |
| 119 | + target.className.includes('-leave') |
| 120 | + ) { |
| 121 | + return |
| 122 | + } |
| 123 | + show.value = false |
| 124 | + nextTick(() => { |
| 125 | + show.value = true |
| 126 | + }) |
| 127 | + } |
| 128 | + |
| 129 | + // Bind events |
| 130 | + target.addEventListener('click', onClick, true) |
| 131 | + // We need delay to check position here |
| 132 | + // since UI may change after click |
| 133 | + rafId = wrapperRaf(() => { |
| 134 | + syncPos() |
| 135 | + }) |
| 136 | + |
| 137 | + if (typeof ResizeObserver !== 'undefined') { |
| 138 | + resizeObserver = new ResizeObserver(syncPos) |
| 139 | + resizeObserver.observe(target) |
| 140 | + } |
| 141 | + } |
| 142 | +} |
| 143 | +onMounted(() => { |
| 144 | + nextTick(() => { |
| 145 | + init() |
| 146 | + }) |
| 147 | +}) |
| 148 | + |
| 149 | +watch( |
| 150 | + () => props.target, |
| 151 | + () => { |
| 152 | + init() |
| 153 | + }, |
| 154 | + { |
| 155 | + flush: 'post', |
| 156 | + }, |
| 157 | +) |
| 158 | + |
| 159 | +onBeforeUnmount(() => { |
| 160 | + clear() |
| 161 | +}) |
| 162 | + |
| 163 | +const onTransitionend = (e: TransitionEvent) => { |
| 164 | + if (e.propertyName === 'opacity') { |
| 165 | + show.value = false |
| 166 | + } |
| 167 | +} |
| 168 | + |
| 169 | +// Auto hide wave after 5 seconds, transition end not work |
| 170 | +watch(show, () => { |
| 171 | + clearTimeout(timeoutId) |
| 172 | + if (show.value) { |
| 173 | + timeoutId = setTimeout(() => { |
| 174 | + show.value = false |
| 175 | + }, 5000) |
| 176 | + } |
| 177 | +}) |
| 178 | + |
| 179 | +const waveStyle = computed(() => { |
| 180 | + const style = { |
| 181 | + left: `${left.value}px`, |
| 182 | + top: `${top.value}px`, |
| 183 | + width: `${width.value}px`, |
| 184 | + height: `${height.value}px`, |
| 185 | + borderRadius: borderRadius.value.map(radius => `${radius}px`).join(' '), |
| 186 | + } |
| 187 | + if (color.value) { |
| 188 | + style['--wave-color'] = color.value |
| 189 | + } |
| 190 | + return style |
| 191 | +}) |
| 192 | +</script> |
0 commit comments