|
|
@@ -1,5 +1,5 @@
|
|
|
<template>
|
|
|
- <view>
|
|
|
+ <view class="cwg-dropdown-root" :data-dropdown-id="dropdownUid">
|
|
|
<view class="cwg-dropdown" :style="customStyle" @click="click">
|
|
|
<slot></slot>
|
|
|
</view>
|
|
|
@@ -19,16 +19,19 @@
|
|
|
</slot>
|
|
|
</view>
|
|
|
</view>
|
|
|
- <view class="cwg-dropdown-mask" :class="{ 'cwg-dropdown-mask-show': maskShow }"
|
|
|
- @click.stop="close" />
|
|
|
+ <view class="cwg-dropdown-mask" :class="{ 'cwg-dropdown-mask-show': maskShow }" />
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, reactive, nextTick, onMounted, onUnmounted } from 'vue'
|
|
|
-import { queryElementRect } from '@/uni_modules/x-tools/tools/sugar.js'
|
|
|
+import { ref, reactive, nextTick, onMounted, onUnmounted, watch, getCurrentInstance } from 'vue'
|
|
|
import { str2px, commonProps } from '@/uni_modules/x-tools/tools/com.js'
|
|
|
|
|
|
+const DROPDOWN_CLOSE_EVENT = 'cwg-dropdown:close-others'
|
|
|
+const DROPDOWN_CLOSE_ALL_EVENT = 'cwg-dropdown:close-all'
|
|
|
+const instance = getCurrentInstance()
|
|
|
+const dropdownUid = `cwg-dropdown-${Math.random().toString(36).slice(2, 9)}`
|
|
|
+
|
|
|
// 合并 props
|
|
|
const props = defineProps({
|
|
|
...commonProps,
|
|
|
@@ -78,6 +81,119 @@ const windowInfo = reactive({
|
|
|
const layout = reactive({})
|
|
|
const innerInterspace = ref(0)
|
|
|
|
|
|
+const TAP_MOVE_THRESHOLD = 10
|
|
|
+const OPEN_GUARD_MS = 300
|
|
|
+let openTimestamp = 0
|
|
|
+let outsideListenersBound = false
|
|
|
+const touchState = {
|
|
|
+ startX: 0,
|
|
|
+ startY: 0,
|
|
|
+ moved: false,
|
|
|
+}
|
|
|
+
|
|
|
+function getTouchPoint(e) {
|
|
|
+ const touch = e.touches?.[0] || e.changedTouches?.[0]
|
|
|
+ return touch ? { x: touch.clientX, y: touch.clientY } : null
|
|
|
+}
|
|
|
+
|
|
|
+function queryInComponent(selector) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ uni.createSelectorQuery()
|
|
|
+ .in(instance?.proxy)
|
|
|
+ .select(selector)
|
|
|
+ .boundingClientRect(resolve)
|
|
|
+ .exec()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function isInsideCurrentDropdown(target) {
|
|
|
+ if (!target?.closest) return false
|
|
|
+ return !!target.closest(`[data-dropdown-id="${dropdownUid}"]`)
|
|
|
+}
|
|
|
+
|
|
|
+const OVERLAY_SELECTORS = [
|
|
|
+ '.cwg-dialog',
|
|
|
+ '.crm-popup',
|
|
|
+ '.uni-popup',
|
|
|
+ '.uni-popup__wrapper',
|
|
|
+ '.uni-modal',
|
|
|
+ '.uni-mask',
|
|
|
+ '.dialog-footer',
|
|
|
+ '.confirm-title',
|
|
|
+ '.confirm-content',
|
|
|
+]
|
|
|
+
|
|
|
+function isInsideOverlay(target) {
|
|
|
+ if (!target?.closest) return false
|
|
|
+ return OVERLAY_SELECTORS.some((selector) => target.closest(selector))
|
|
|
+}
|
|
|
+
|
|
|
+function onOutsideTouchStart(e) {
|
|
|
+ if (!maskShow.value) return
|
|
|
+ const point = getTouchPoint(e)
|
|
|
+ if (!point) return
|
|
|
+ touchState.startX = point.x
|
|
|
+ touchState.startY = point.y
|
|
|
+ touchState.moved = false
|
|
|
+}
|
|
|
+
|
|
|
+function onOutsideTouchMove(e) {
|
|
|
+ if (!maskShow.value) return
|
|
|
+ const point = getTouchPoint(e)
|
|
|
+ if (!point) return
|
|
|
+ if (
|
|
|
+ Math.abs(point.x - touchState.startX) > TAP_MOVE_THRESHOLD
|
|
|
+ || Math.abs(point.y - touchState.startY) > TAP_MOVE_THRESHOLD
|
|
|
+ ) {
|
|
|
+ touchState.moved = true
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onOutsideTouchEnd(e) {
|
|
|
+ if (!maskShow.value) return
|
|
|
+ if (Date.now() - openTimestamp < OPEN_GUARD_MS) return
|
|
|
+ if (touchState.moved) return
|
|
|
+ if (isInsideCurrentDropdown(e.target) || isInsideOverlay(e.target)) return
|
|
|
+ e.preventDefault()
|
|
|
+ e.stopPropagation()
|
|
|
+ close()
|
|
|
+}
|
|
|
+
|
|
|
+function onOutsideClick(e) {
|
|
|
+ if (!maskShow.value) return
|
|
|
+ if (Date.now() - openTimestamp < OPEN_GUARD_MS) return
|
|
|
+ if (isInsideCurrentDropdown(e.target) || isInsideOverlay(e.target)) return
|
|
|
+ e.preventDefault()
|
|
|
+ e.stopPropagation()
|
|
|
+ close()
|
|
|
+}
|
|
|
+
|
|
|
+function bindOutsideListeners() {
|
|
|
+ if (outsideListenersBound || typeof document === 'undefined') return
|
|
|
+ outsideListenersBound = true
|
|
|
+ document.addEventListener('touchstart', onOutsideTouchStart, true)
|
|
|
+ document.addEventListener('touchmove', onOutsideTouchMove, { capture: true, passive: true })
|
|
|
+ document.addEventListener('touchend', onOutsideTouchEnd, { capture: true, passive: false })
|
|
|
+ document.addEventListener('click', onOutsideClick, true)
|
|
|
+}
|
|
|
+
|
|
|
+function unbindOutsideListeners() {
|
|
|
+ if (!outsideListenersBound || typeof document === 'undefined') return
|
|
|
+ outsideListenersBound = false
|
|
|
+ document.removeEventListener('touchstart', onOutsideTouchStart, true)
|
|
|
+ document.removeEventListener('touchmove', onOutsideTouchMove, true)
|
|
|
+ document.removeEventListener('touchend', onOutsideTouchEnd, true)
|
|
|
+ document.removeEventListener('click', onOutsideClick, true)
|
|
|
+}
|
|
|
+
|
|
|
+watch(maskShow, (visible) => {
|
|
|
+ if (visible) {
|
|
|
+ bindOutsideListeners()
|
|
|
+ } else {
|
|
|
+ unbindOutsideListeners()
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
// 获取系统信息
|
|
|
const getSystemInfo = () => {
|
|
|
return new Promise((resolve) => {
|
|
|
@@ -101,8 +217,13 @@ const getSystemInfo = () => {
|
|
|
// 点击触发器
|
|
|
const click = async (e) => {
|
|
|
e.stopPropagation()
|
|
|
+ if (maskShow.value) {
|
|
|
+ close()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.$emit(DROPDOWN_CLOSE_EVENT, dropdownUid)
|
|
|
await getSystemInfo()
|
|
|
- const triggerRect = await queryElementRect('.cwg-dropdown')
|
|
|
+ const triggerRect = await queryInComponent('.cwg-dropdown')
|
|
|
if (!triggerRect) return
|
|
|
const tempStyle = {
|
|
|
transform: 'scaleY(1)',
|
|
|
@@ -113,7 +234,7 @@ const click = async (e) => {
|
|
|
}
|
|
|
Object.assign(layout, tempStyle)
|
|
|
await nextTick()
|
|
|
- const menuRect = await queryElementRect('.cwg-dropdown-menu-container')
|
|
|
+ const menuRect = await queryInComponent('.cwg-dropdown-menu-container')
|
|
|
if (!menuRect) {
|
|
|
Object.keys(layout).forEach(key => delete layout[key])
|
|
|
layout.transform = 'scaleY(0)'
|
|
|
@@ -135,6 +256,7 @@ const click = async (e) => {
|
|
|
}
|
|
|
Object.keys(layout).forEach(key => delete layout[key])
|
|
|
Object.assign(layout, finalLayout)
|
|
|
+ openTimestamp = Date.now()
|
|
|
maskShow.value = true
|
|
|
emit('open')
|
|
|
emit('change', true)
|
|
|
@@ -145,32 +267,35 @@ const menuClick = (value, index) => {
|
|
|
close()
|
|
|
}
|
|
|
const close = () => {
|
|
|
- console.log('关闭弹窗2')
|
|
|
+ if (!maskShow.value && Object.keys(layout).length === 0) return
|
|
|
maskShow.value = false
|
|
|
Object.keys(layout).forEach(key => delete layout[key])
|
|
|
layout.transform = 'scaleY(0)'
|
|
|
- console.log('弹窗',layout)
|
|
|
emit('close')
|
|
|
emit('change', false)
|
|
|
}
|
|
|
+
|
|
|
+function onCloseOthers(activeUid) {
|
|
|
+ if (activeUid !== dropdownUid) {
|
|
|
+ close()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
getSystemInfo()
|
|
|
innerInterspace.value = str2px(props.interspace)
|
|
|
+
|
|
|
+ uni.$on(DROPDOWN_CLOSE_EVENT, onCloseOthers)
|
|
|
+ uni.$on(DROPDOWN_CLOSE_ALL_EVENT, close)
|
|
|
|
|
|
- uni.$on('logout', () => {
|
|
|
- console.log('关闭弹窗1')
|
|
|
- if (maskShow.value || Object.keys(layout).length > 0) {
|
|
|
- maskShow.value = false
|
|
|
- Object.keys(layout).forEach(key => delete layout[key])
|
|
|
- layout.transform = 'scaleY(0)'
|
|
|
- emit('close')
|
|
|
- emit('change', false)
|
|
|
- }
|
|
|
- })
|
|
|
+ uni.$on('logout', close)
|
|
|
})
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
- uni.$off('logout')
|
|
|
+ uni.$off('logout', close)
|
|
|
+ uni.$off(DROPDOWN_CLOSE_EVENT, onCloseOthers)
|
|
|
+ uni.$off(DROPDOWN_CLOSE_ALL_EVENT, close)
|
|
|
+ unbindOutsideListeners()
|
|
|
})
|
|
|
// 暴露方法
|
|
|
defineExpose({
|
|
|
@@ -190,6 +315,7 @@ defineExpose({
|
|
|
.cwg-dropdown-menu {
|
|
|
position: relative;
|
|
|
z-index: 999;
|
|
|
+ pointer-events: none;
|
|
|
}
|
|
|
|
|
|
.cwg-dropdown-mask {
|
|
|
@@ -207,13 +333,16 @@ defineExpose({
|
|
|
|
|
|
.cwg-dropdown-mask-show {
|
|
|
opacity: 1;
|
|
|
- pointer-events: auto;
|
|
|
+ pointer-events: none;
|
|
|
+ touch-action: pan-y;
|
|
|
+ -webkit-touch-callout: none;
|
|
|
}
|
|
|
|
|
|
.cwg-dropdown-menu-container {
|
|
|
position: absolute;
|
|
|
transform-origin: top;
|
|
|
transform: scaleY(0);
|
|
|
+ pointer-events: auto;
|
|
|
|
|
|
.menu {
|
|
|
position: relative;
|