Explorar el Código

feat: 滚动条支持, 边缘限制
1. 拖动时支持两个方向上的支持
2. 碰撞检测
3. 有滚动条的情况下自动滚动元素

kindring hace 10 meses
padre
commit
c266d145e6

+ 98 - 6
src/components/magnetView.vue

@@ -1,11 +1,12 @@
 <script setup lang="ts">
-import { shallowRef} from "vue";
+import {ref, shallowRef} from "vue";
 import TimeMagnet from "@/components/magnets/timeMagnet.vue";
-import vueDrag from "@/components/public/vueDrag.vue";
+import vueDrag, {MoveInfo} from "@/components/public/vueDrag.vue";
 import {Magnet, MagnetEmit, MagnetSize} from "@/types/magnetType.ts";
-import {computeMagnetStyle, initTimeMagnetInfo} from "@/components/magnets/magnetInfo.ts";
+import {computeMagnetStyle, comXY, initTimeMagnetInfo} from "@/components/magnets/magnetInfo.ts";
 
 import {Calendar} from "@/util/time.ts";
+import {CollisionResult, detectCollisionDirection, Rect} from "@/util/domDrag.ts";
 
 const timeMagnetInfo = initTimeMagnetInfo(TimeMagnet)
 
@@ -36,6 +37,18 @@ const magnetItemInfos: vueMagnet[] = [
     editMode: false,
     selected: false,
     component: timeMagnetInfo.component
+  },
+  {
+    id: `2323`,
+    type: timeMagnetInfo.type,
+    x: 5,
+    y: 20,
+    width: timeMagnetInfo.sizes.small?.width??0,
+    height: timeMagnetInfo.sizes.small?.height??0,
+    size: MagnetSize.small,
+    editMode: false,
+    selected: false,
+    component: timeMagnetInfo.component
   }
 ];
 
@@ -64,7 +77,60 @@ defineProps({
   }
 })
 
+// 转换为坐标值
 
+const moveTipStyle = ref()
+
+function moveHandle(magnet: vueMagnet, moveInfo: MoveInfo){
+  // 计算新位置相对于多少值
+  let {x, y} = comXY(moveInfo.left, moveInfo.top)
+  let magnetInfo: vueMagnet = {
+    ...magnet,
+    x: x,
+    y: y
+  }
+  let moveStyle = computeMagnetStyle(magnetInfo)
+  moveTipStyle.value = {
+    ...moveStyle,
+    opacity: `1`
+  }
+  // 判断当前元素是否占用到对应元素
+  // 移动占位元素
+  // console.log(magnetInfo)
+  comMagnets(magnetInfo)
+}
+
+// 计算其他元素的位置
+function comMagnets(changeMagnet: vueMagnet){
+  let changeMagnetRect: Rect = {
+    ...changeMagnet
+  }
+  // 挤开当前元素所在位置
+  for (let i = 0; i < magnetItems.value.length; i++)
+  {
+    let magnet = magnetItems.value[i]
+    if(magnet.id === changeMagnet.id) continue
+    // 判断当前元素是否被其他元素占用了
+    // 转换为 rect
+    let magnetRect: Rect  = {
+      ...magnet
+    }
+    let collisionResult: CollisionResult = detectCollisionDirection(changeMagnetRect, magnetRect)
+    if(collisionResult.colliding)
+    {
+      console.log(`改位置已经有元素 方向${collisionResult.direction}`);
+      // console.log(magnet)
+    }
+  }
+}
+
+function moveEndHandle(magnet: vueMagnet, moveInfo: MoveInfo)
+{
+  console.log(magnet, moveInfo)
+  moveTipStyle.value = {
+    opacity: `0`
+  }
+}
 
 </script>
 
@@ -72,14 +138,22 @@ defineProps({
   <!-- 磁帖 布局组件, -->
   <div class="magnet scroll">
     <!--    磁贴组件布局 -->
-
+    <div class="magnet-move"
+         v-show="editMode"
+         :style="moveTipStyle"
+    ></div>
     <vue-drag class="magnet-item"
        v-for="magnet in magnetItems"
        :key="magnet.id"
        :style="computeMagnetStyle(magnet)"
        :open-drag="editMode"
+       :move-hide="true"
+       :y-limit="false"
+       @move="(moveInfo)=>{moveHandle(magnet, moveInfo)}"
+       @move-end="(moveInfo)=>{moveEndHandle(magnet, moveInfo)}"
     >
       <component
+          v-show="!editMode"
           :is="magnet.component"
           :size="magnet.size"
           @magnet="eventHandler"
@@ -97,8 +171,6 @@ defineProps({
   overflow-y: auto;
 }
 
-
-
 .magnet-item {
   position: absolute;
   border-radius: 6px;
@@ -121,4 +193,24 @@ defineProps({
   opacity: 0.5;
 }
 
+.magnet-move{
+  position: absolute;
+  border-radius: 6px;
+  box-shadow: 0 1px 2px #18d8ea;
+  border: 1px solid #18d8ea;
+  overflow: hidden;
+  opacity: 0;
+}
+.magnet-move::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: #000;
+  opacity: 0.3;
+}
+
+
 </style>

+ 6 - 1
src/components/magnets/magnetInfo.ts

@@ -65,7 +65,12 @@ export function computeStyle(w: number, h: number, x: number, y: number, sub: bo
     }
 }
 
-
+// 通过元素位置反向计算xy值
+export function comXY(left: number, top: number){
+    let x = Math.floor(left / (cellWidth + cellMargin));
+    let y = Math.floor(top / (cellWidth + cellMargin));
+    return {x, y}
+}
 
 
 

+ 137 - 32
src/components/public/vueDrag.vue

@@ -1,6 +1,11 @@
 <script setup lang="ts">
 import {ref, watch} from 'vue'
 import {Drag, MouseInfo, ElementInfo} from '@/util/domDrag.ts'
+
+export interface MoveInfo extends MouseInfo{
+  left: number,
+  top: number,
+}
 const dragRef = ref<HTMLElement>()
 // init drag
 
@@ -13,8 +18,23 @@ const props = defineProps({
   moveHide: {
     type: Boolean,
     default: false
+  },
+  xLimit: {
+    type: Boolean,
+    default: true
+  },
+  yLimit: {
+    type: Boolean,
+    default: true
   }
 })
+
+const emits = defineEmits<{
+  (e: 'move-start', moveInfo: MoveInfo ): void,
+  (e: 'move', moveInfo: MoveInfo ): void,
+  (e: 'move-end', moveInfo: MoveInfo ): void,
+}>()
+
 let drag: Drag | null = null
 
 function initDrag() {
@@ -42,26 +62,95 @@ moveStyle.value = {
   top: `10px`
 }
 
-let elementDisplayValue = ""
+let elementOpacityValue = ""
 let parentPositionValue = ""
 // 直接添加至父元素中的临时拷贝dom
-let templateElement = null;
+let templateElement: HTMLElement | null  = null;
+
+function _setDomStyle(el: HTMLElement, mouseInfo: MouseInfo, dragInfo: ElementInfo, isEnd: boolean = false,
+                      xLimit: boolean = false, yLimit: boolean = false): MoveInfo{
+  let elLeft = mouseInfo.x - dragInfo.diffX -  dragInfo.parentLeft;
+  let elTop = mouseInfo.y - dragInfo.diffY -  dragInfo.parentTop;
+  let parentEl = el.offsetParent as HTMLElement || document.body;
+
+  let parentTotalWidth = parentEl.scrollWidth;
+  let parentTotalHeight = parentEl.scrollHeight;
+  let parentWidth = parentEl.clientWidth;
+  let parentHeight = parentEl.clientHeight;
+  let parentScrollTop = parentEl.scrollTop || 0;
+  let parentScrollLeft = parentEl.scrollLeft || 0;
+
+  elLeft = Math.max(0, elLeft);
+  elTop = Math.max(0, elTop);
+
+  if (xLimit)
+  {
+    elLeft = Math.min(elLeft, parentTotalWidth - dragInfo.width);
+  }
+  if (yLimit)
+  {
+    elTop = Math.min(elTop, parentTotalHeight - dragInfo.height);
+  }
+
+  if (elLeft + dragInfo.width > parentWidth)
+  {
+    parentEl.scrollLeft = elLeft + dragInfo.width - parentWidth;
+  }if (elLeft < parentScrollLeft)
+  {
+    parentEl.scrollLeft = elLeft;
+  }
+
+  if (elTop + dragInfo.height > parentHeight + parentScrollTop)
+  {
+    // console.log(` 滚动条更改 ${elTop} ${parentScrollTop}`)
+    parentEl.scrollTop = elTop + dragInfo.height - parentHeight;
+  }if (elTop < parentScrollTop)
+  {
+    parentEl.scrollTop = elTop;
+  }
+
+
+
+
+  el.style.position = "absolute";
+  el.style.left = `${elLeft}px`;
+  el.style.top = `${elTop}px`;
+
+  if (isEnd)
+  {
+    el.style.transition = "all 0.3s";
+  }
+
+  let moveInfo = {
+    ...mouseInfo,
+    left: elLeft,
+    top: elTop
+  };
+
+  return moveInfo;
+}
+
+
 
 function moveStartHandle(mouseInfo: MouseInfo, dragInfo: ElementInfo){
   console.log('开始移动')
-  console.log(mouseInfo)
-  console.log(dragInfo)
+  // console.log(mouseInfo)
+  // console.log(dragInfo)
   // 存储当前元素的样式
   let el = dragInfo.el;
   if(!el){
     return console.log("未知的dom元素")
   }
-  let parentEl = el.offsetParent;
-  templateElement = el.cloneNode(true);
-  // 移除克隆的dom元素的所有事件
-  templateElement.style.position = "absolute";
-  templateElement.style.left = `${mouseInfo.x - dragInfo.diffX -  dragInfo.parentLeft}px`;
-  templateElement.style.top = `${mouseInfo.y - dragInfo.diffY -  dragInfo.parentTop}px`;
+  let parentEl = el.offsetParent as HTMLElement;
+  if (!parentEl)
+  {
+    parentEl = document.body;
+  }
+  templateElement = el.cloneNode(true) as HTMLElement;
+
+  let moveInfo = _setDomStyle(templateElement, mouseInfo, dragInfo, false, props.xLimit, props.yLimit);
+  emits('move-start', moveInfo)
+
   // 如果父元素无定位根基属性
   parentPositionValue = parentEl.style.position;
   if(parentPositionValue != "absolute" && parentPositionValue != "relative" && parentPositionValue != "fixed" )
@@ -70,43 +159,59 @@ function moveStartHandle(mouseInfo: MouseInfo, dragInfo: ElementInfo){
   }
   parentEl.append(templateElement)
   if(props.moveHide){
-    elementDisplayValue = el.style.display
+    elementOpacityValue = el.style.opacity
     // 获取原有的 opacity 和 display 信息
-    el.style.display = "none"
+    el.style.opacity = "0"
   }
   return true;
 }
 
 function moveHandle(mouseInfo: MouseInfo, dragInfo: ElementInfo){
-  console.log('move')
-  console.log(mouseInfo)
-  console.log(dragInfo)
-  templateElement.style.position = "absolute";
-  templateElement.style.left = `${mouseInfo.x - dragInfo.diffX - dragInfo.parentLeft}px`;
-  templateElement.style.top = `${mouseInfo.y - dragInfo.diffY - dragInfo.parentTop}px`;
+  // console.log(mouseInfo)
+  // console.log(dragInfo)
+  if(templateElement == null) return console.log("未知的dom元素")
+  // 组件超出限制
+
+  let moveInfo = _setDomStyle(templateElement, mouseInfo, dragInfo, false, props.xLimit, props.yLimit);
+  emits('move', moveInfo)
+
 }
 
 function moveEndHandle(mouseInfo: MouseInfo, dragInfo: ElementInfo){
   console.log('结束移动')
-  console.log(mouseInfo)
-  console.log(dragInfo)
+  // console.log(mouseInfo)
+  // console.log(dragInfo)
   let el = dragInfo.el;
   if(!el){
-    return console.log("未知的dom元素")
-  }
-  let parentEl = el.offsetParent;
-  if(props.moveHide){
-    // 获取原有的 opacity 和 display 信息
-    el.style.display = elementDisplayValue
-    elementDisplayValue = ""
+    return console.log("未知的父级元素")
   }
-  // 移除临时dom元素
-  templateElement.remove()
-  moveStyle.value = {}
-  if(parentPositionValue != "absolute" && parentPositionValue != "relative" && parentPositionValue != "fixed" )
+  let parentEl = el.offsetParent as HTMLElement;
+  if (!parentEl)
   {
-    parentEl.style.position = parentPositionValue
+    parentEl = document.body;
   }
+
+  if(templateElement == null) return console.log("未知的dom元素")
+  let element = templateElement;
+  // 恢复元素位置
+  let moveInfo = _setDomStyle(templateElement, mouseInfo, dragInfo, true, props.xLimit, props.yLimit);
+  emits('move-end', moveInfo)
+  // 添加动画
+
+  setTimeout(() => {
+    // 移除临时dom元素
+    element.remove()
+    moveStyle.value = {}
+    if(parentPositionValue != "absolute" && parentPositionValue != "relative" && parentPositionValue != "fixed" )
+    {
+      parentEl.style.position = parentPositionValue
+    }
+    if(props.moveHide){
+      // 获取原有的 opacity 和 display 信息
+      el.style.opacity = elementOpacityValue
+      elementOpacityValue = ""
+    }
+  }, 1000 * 0.3)
 }
 
 

+ 105 - 43
src/util/domDrag.ts

@@ -1,3 +1,27 @@
+
+export interface ElementInfo{
+    el: HTMLElement | null;
+    left: number;
+    top: number;
+    parentLeft: number;
+    parentTop: number;
+    width: number;
+    height: number;
+    // 鼠标点击位置与元素位置的差值
+    diffX: number;
+    diffY: number;
+
+}
+
+export interface MouseInfo{
+    x: number;
+    y: number;
+}
+
+export interface MouseListener{
+    (mouse: MouseInfo, el: ElementInfo): void;
+}
+
 // 元素拖动
 function getElementLeft(el: HTMLElement): number{
     let left = el.offsetLeft || 0;
@@ -25,34 +49,87 @@ function getElementDistanceToViewportEdge(element: HTMLElement): { top: number,
     const right = viewportWidth - rect.right + scrollX;
     const bottom = viewportHeight - rect.bottom + scrollY;
     const left = rect.left - scrollX;
-
     return { top, right, bottom, left };
 }
+// 获取元素滚动条位置
+function getScrollLeft(el: HTMLElement): number{
+    return el.scrollLeft || 0;
+}
+function getScrollTop(el: HTMLElement): number{
+    return el.scrollTop || 0;
+}
 
+/**
+ * 鼠标位置基于滚动条偏移
+ * @param mouseInfo 直接修改此对象
+ * @param el
+ */
+function comMouseInfo(mouseInfo: MouseInfo, el?: HTMLElement | null): MouseInfo{
+    // 父元素有滚动条的情况下,需要减去滚动条的偏移量
+    if (el) {
+        const scrollX = getScrollLeft(el);
+        const scrollY = getScrollTop(el);
+        // console.log(`scrollX: ${scrollX} scrollY: ${scrollY}`);
+        mouseInfo.x += scrollX;
+        mouseInfo.y += scrollY;
+    }
+    return mouseInfo;
+}
 
+enum CollisionDirection {
+    None = 0,
+    Top = 1,
+    Bottom = 2,
+    Left = 3,
+    Right = 4
+}
 
-export interface ElementInfo{
-    el: HTMLElement | null;
-    left: number;
-    top: number;
-    parentLeft: number;
-    parentTop: number;
-    width: number;
-    height: number;
-    // 鼠标点击位置与元素位置的差值
-    diffX: number;
-    diffY: number;
-
+export interface CollisionResult {
+    colliding: boolean;
+    direction: CollisionDirection;
 }
 
-export interface MouseInfo{
+export interface Rect {
     x: number;
     y: number;
+    width: number;
+    height: number;
 }
 
-export interface MouseListener{
-    (mouse: MouseInfo, el: ElementInfo): void;
+export function detectCollisionDirection(rect1: Rect, rect2: Rect): CollisionResult {
+    const horizontalOverlap = Math.min(rect1.x + rect1.width, rect2.x + rect2.width) - Math.max(rect1.x, rect2.x);
+    const verticalOverlap = Math.min(rect1.y + rect1.height, rect2.y + rect2.height) - Math.max(rect1.y, rect2.y);
+
+    if (horizontalOverlap > 0 && verticalOverlap > 0) {
+        if (horizontalOverlap > verticalOverlap) {
+            return {
+                colliding: true,
+                direction: rect1.y < rect2.y ? CollisionDirection.Top : CollisionDirection.Bottom
+            };
+        } else {
+            return {
+                colliding: true,
+                direction: rect1.x < rect2.x ? CollisionDirection.Left : CollisionDirection.Right
+            };
+        }
+    }
+
+    return { colliding: false, direction: CollisionDirection.None };
+}
+
+
+
+export function isColliding(rect1: Rect, rect2: Rect): boolean {
+    if (rect1.x < rect2.x + rect2.width &&
+        rect1.x + rect1.width > rect2.x &&
+        rect1.y < rect2.y + rect2.height &&
+        rect1.y + rect1.height > rect2.y) {
+        return true;
+    } else {
+        return false;
+    }
 }
+
 export class Drag{
     constructor(el: HTMLElement, moveWait: number = 60) {
         // 绑定事件
@@ -101,13 +178,13 @@ export class Drag{
     //
     private bindEvent() {
         const el = this.el;
-        el.addEventListener('mousedown', this.downEvent);
+        el.addEventListener('mousedown', this.downEvent)
+        // todo 触摸支持
         let parent = el.parentElement;
         if (!parent) {
             parent = document.body;
         }
         const parentDistance = getElementDistanceToViewportEdge(parent);
-        const distance = getElementDistanceToViewportEdge(el);
         this.parent = {
             el: parent,
             left: parentDistance.left,
@@ -132,10 +209,6 @@ export class Drag{
         }
 
 
-        console.log('Distance to top:', distance.top);
-        console.log('Distance to right:', distance.right);
-        console.log('Distance to bottom:', distance.bottom);
-        console.log('Distance to left:', distance.left);
     }
     downEvent = (e: MouseEvent) => {
         if(this.isMove){
@@ -144,24 +217,15 @@ export class Drag{
         // 获取鼠标位置
         const x = e.clientX;
         const y = e.clientY;
-        let el = this.el;
         this.isMove = true;
 
-        console.log(el);
-        // 获取元素再页面上的位置
-        const left = getElementLeft(el);
-        const top = getElementTop(el);
-        let parentLeft = 0;
-        let parentTop = 0;
-
-
-        console.log(left, top)
-        console.log(parentLeft, parentTop)
-
         // 计算元素偏移差值
         const diffX = e.offsetX;
         const diffY =  e.offsetY;
-
+        let mouseInfo: MouseInfo = {
+            x: x,
+            y: y
+        }
         this.startX = x;
         this.startY = y;
 
@@ -170,15 +234,14 @@ export class Drag{
             diffX: diffX,
             diffY: diffY
         }
+
         if (this.parent.el) {
             const parentDistance = getElementDistanceToViewportEdge(this.parent.el)
             this.thisInfo.parentTop = parentDistance.top
             this.thisInfo.parentLeft = parentDistance.left
+            comMouseInfo(mouseInfo, this.parent.el);
         }
-        let mouseInfo: MouseInfo = {
-            x: x,
-            y: y
-        }
+
         this.moveStart && this.moveStart(mouseInfo, this.thisInfo);
         setTimeout(()=>{
             document.addEventListener('mousemove', this.mouseMove);
@@ -192,7 +255,6 @@ export class Drag{
         // 获取鼠标位置
         const x = e.clientX;
         const y = e.clientY;
-        console.log(`mouse up event x: ${x} y: ${y}`)
         let mouseInfo: MouseInfo = {
             x: x,
             y: y
@@ -209,13 +271,13 @@ export class Drag{
             return
         }
         // 获取鼠标位置
-        const x = e.clientX;
-        const y = e.clientY;
-        console.log(`mouse move event x: ${x} y: ${y}`)
+        const x = e.pageX;
+        const y = e.pageY;
         let mouseInfo: MouseInfo = {
             x: x,
             y: y
         }
+        comMouseInfo(mouseInfo, this.parent.el);
         this.move && this.move(mouseInfo, this.thisInfo);
     }