|
@@ -0,0 +1,162 @@
|
|
|
|
+<template>
|
|
|
|
+ <teleport :to="mount">
|
|
|
|
+ <div class="ui-floating" :style="style" :class="props.class" @mouseenter="emit('enter')" @mouseleave="emit('leave')">
|
|
|
|
+ <slot></slot>
|
|
|
|
+ </div>
|
|
|
|
+ </teleport>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script setup>
|
|
|
|
+// onUpdated
|
|
|
|
+import { defineProps, defineExpose, onUnmounted, reactive, watch, computed, onActivated } from 'vue'
|
|
|
|
+import { getPostionByTarget, getScrollParents, getZIndex } from '../../utils'
|
|
|
|
+
|
|
|
|
+const Horizontal = {
|
|
|
|
+ center: 'center',
|
|
|
|
+ right: 'right',
|
|
|
|
+ left: 'left',
|
|
|
|
+}
|
|
|
|
+const Vertical = {
|
|
|
|
+ center: 'center',
|
|
|
|
+ top: 'top',
|
|
|
|
+ bottom: 'bottom',
|
|
|
|
+}
|
|
|
|
+const Divide = '-'
|
|
|
|
+
|
|
|
|
+const props = defineProps({
|
|
|
|
+ mount: {
|
|
|
|
+ require: true,
|
|
|
|
+ default: document.body,
|
|
|
|
+ },
|
|
|
|
+ class: { type: String },
|
|
|
|
+ refer: { type: Object },
|
|
|
|
+ dire: { type: String },
|
|
|
|
+ width: { type: [Number, String] },
|
|
|
|
+ height: { type: [Number, String] },
|
|
|
|
+})
|
|
|
|
+const emit = defineEmits(['leave', 'enter'])
|
|
|
|
+
|
|
|
|
+// 确定方向
|
|
|
|
+const dires = computed(() => {
|
|
|
|
+ const dire = props.dire || `${Vertical.bottom}${Divide}${Horizontal.left}`
|
|
|
|
+ const isPreset = (preset, val) => Object.keys(preset).some(key => preset[key] === val)
|
|
|
|
+
|
|
|
|
+ let [horizontal, vertical] = dire.split(Divide)
|
|
|
|
+
|
|
|
|
+ if (!horizontal || !isPreset(Horizontal, horizontal)) {
|
|
|
|
+ horizontal = Horizontal.left
|
|
|
|
+ }
|
|
|
|
+ if (!vertical || !isPreset(Vertical, vertical)) {
|
|
|
|
+ vertical = Vertical.bottom
|
|
|
|
+ }
|
|
|
|
+ return [horizontal, vertical]
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+const normalizeUnit = (unit, total) => {
|
|
|
|
+ if (unit === void 0) {
|
|
|
|
+ return void 0
|
|
|
|
+ } else if (typeof unit === 'number') {
|
|
|
|
+ return unit ? ((unit <= 1) & (unit >= 0) ? total * unit : unit) : void 0
|
|
|
|
+ } else if (unit.includes('px')) {
|
|
|
|
+ return normalizeUnit(parseFloat(unit), total)
|
|
|
|
+ } else if (unit.includes('%')) {
|
|
|
|
+ return normalizeUnit(parseFloat(unit) / 100, total)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const width = computed(() => props.refer && normalizeUnit(props.width, props.refer.offsetWidth))
|
|
|
|
+const height = computed(() => props.refer && normalizeUnit(props.height, props.refer.offsetHeight))
|
|
|
|
+
|
|
|
|
+const location = reactive({ x: 0, y: 0 })
|
|
|
|
+const scrollParents = computed(() => (props.refer ? getScrollParents(props.refer, props.mount) : []))
|
|
|
|
+
|
|
|
|
+watch(
|
|
|
|
+ [scrollParents, props],
|
|
|
|
+ ([newParents], [oldParents]) => {
|
|
|
|
+ oldParents && oldParents.forEach(dom => dom.removeEventListener('scroll', updateLocation))
|
|
|
|
+ newParents.forEach(dom => dom.addEventListener('scroll', updateLocation))
|
|
|
|
+ if (props.refer) {
|
|
|
|
+ setTimeout(() => updateLocation())
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ { immediate: true }
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+const zIndex = getZIndex()
|
|
|
|
+
|
|
|
|
+const style = computed(() => {
|
|
|
|
+ let style = {
|
|
|
|
+ width: width.value && width.value + 'px',
|
|
|
|
+ height: height.value && height.value + 'px',
|
|
|
|
+ left: location.x + 'px',
|
|
|
|
+ top: location.y + 'px',
|
|
|
|
+ zIndex: zIndex,
|
|
|
|
+ }
|
|
|
|
+ if (location.x > 0 && location.y > 0) {
|
|
|
|
+ return style
|
|
|
|
+ }
|
|
|
|
+ return {}
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+const updateLocation = () => {
|
|
|
|
+ const pos = getPostionByTarget(props.refer, props.mount)
|
|
|
|
+ const screenInfo = scrollParents.value.reduce(
|
|
|
|
+ (t, c) => {
|
|
|
|
+ t.y += c.scrollTop
|
|
|
|
+ t.x += c.scrollLeft
|
|
|
|
+ return t
|
|
|
|
+ },
|
|
|
|
+ { x: 0, y: 0 }
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ const [horizontal, vertical] = dires.value
|
|
|
|
+ const start = {
|
|
|
|
+ x: pos.x - screenInfo.x,
|
|
|
|
+ y: pos.y - screenInfo.y,
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ switch (horizontal) {
|
|
|
|
+ case Horizontal.left:
|
|
|
|
+ location.x = start.x
|
|
|
|
+ break
|
|
|
|
+ case Horizontal.right:
|
|
|
|
+ location.x = start.x + pos.width
|
|
|
|
+ break
|
|
|
|
+ case Horizontal.center:
|
|
|
|
+ location.x = start.x + pos.width / 2
|
|
|
|
+ break
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ switch (vertical) {
|
|
|
|
+ case Vertical.top:
|
|
|
|
+ location.y = start.y
|
|
|
|
+ break
|
|
|
|
+ case Vertical.bottom:
|
|
|
|
+ location.y = start.y + pos.height
|
|
|
|
+ break
|
|
|
|
+ case Vertical.center:
|
|
|
|
+ location.y = start.y + pos.height / 2
|
|
|
|
+ break
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+window.addEventListener('resize', updateLocation)
|
|
|
|
+onUnmounted(() => {
|
|
|
|
+ scrollParents.value.forEach(dom => dom.removeEventListener('scroll', updateLocation))
|
|
|
|
+ window.removeEventListener('resize', updateLocation)
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+onActivated(() => {
|
|
|
|
+ if (props.refer) {
|
|
|
|
+ updateLocation()
|
|
|
|
+ }
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+defineExpose({
|
|
|
|
+ updateLocation,
|
|
|
|
+})
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<script>
|
|
|
|
+export default { name: 'UiFloating' }
|
|
|
|
+</script>
|