元素缩放?一个vue指令搞定

47 篇文章 3 订阅

说在前面

🎈元素拖拽缩放功能大家都不陌生吧?今天我们一起来看看怎么简单编写一个vue指令来实现元素拖拽缩放功能。

效果展示

体验地址

http://jyeontu.xyz/jvuewheel/#/JZoomView

实现思路

1、自定义指令对象

export default {
  inserted(el, binding) {
    // ...
  }
};

这里定义了一个Vue自定义指令,并通过inserted钩子函数,在元素被插入到DOM时执行相关的逻辑。

2、生成随机id

const getRandom = (min, max) => {
    return Math.floor(Math.random() * (max - min) + min);
};

生成两个指定值 minmax 之间的随机整数

3、套盒子

为了方便后续位置计算添加锚点,我们可以先给需要添加指令的元素外层套上两层盒子。

const box = document.createElement("div");
box.style.position = "relative";
box.classList = "j-zoom-box";

el.style.position = "relative";
const bounding = el.getBoundingClientRect();
const content = document.createElement("div");
content.classList = "j-zoom-content";
const parentElement = el.parentElement;
parentElement.removeChild(el);
content.appendChild(el);
content.style.position = "absolute";
content.style.height = bounding.height + "px";
content.style.width = bounding.width + "px";
box.style.height = bounding.height + "px";
box.style.width = bounding.width + "px";
el.style.height = "100%";
el.style.width = "100%";
box.appendChild(content);
parentElement.appendChild(box);

如下图:

通过这种设置,el被放置在一个可以独立操作的容器(box)内,例如缩放或移动,同时el保持其原始大小和相对于content的位置。使用绝对定位的content和相对定位的box和el确保了el可以在content内部被定位和缩放,而不影响其他元素的布局。

4、添加角锚点

添加四个元素,分别定位到四个顶角,给四个顶角设置不同的鼠标样式并添加鼠标点击或触屏开始事件。

function createHornRect(position) {
    const isTop = position.includes("top");
    const rect = document.createElement("div");
    rect.style.width = rectWidth + "px";
    rect.style.height = rectWidth + "px";
    rect.style.position = "absolute";
    rect.style.background = "transparent";
    if (isTop) rect.style.top = 0;
    else rect.style.bottom = 0;
    rect.style.left = position.includes("Left") ? "0" : "";
    rect.style.right = position.includes("Right") ? "0" : "";
    const map = {
        topLeft: "nw",
        topRight: "ne",
        bottomLeft: "sw",
        bottomRight: "se",
    };
    rect.style.cursor = map[position] + "-resize";
    return rect;
}
function createAndAddHorn(position) {
    const horn = createHornRect(position);
    content.appendChild(horn);
    horn.addEventListener("mousedown", (e) => {
        zoomDomMouseDown(e, position);
    });
    horn.addEventListener("touchstart", (e) => {
        zoomDomMouseDown(e, position);
    });
}

["topLeft", "topRight", "bottomLeft", "bottomRight"].forEach(
    (position) => {
        createAndAddHorn(position);
    }
);

四个角的锚点位置如下图:

5、添加边元素

添加四个元素,分别定位到四个边上,注意留出顶角的位置,给四个边缘元素设置不同的鼠标样式并添加鼠标点击或触屏开始事件。

function createEdgeElement(position) {
    const edge = document.createElement("div");
    edge.id = position + "Mid" + randomId;
    let height = rectWidth,
        width = rectWidth;
    if (position === "top" || position === "bottom")
        width = bounding.width - rectWidth * 2;
    else height = bounding.height - rectWidth * 2;
    const cursorMap = {
        top: "n-resize",
        bottom: "s-resize",
        left: "w-resize",
        right: "e-resize",
    };
    obj2Style(edge, {
        [["top", "bottom"].includes(position) ? "left" : "top"]:
            rectWidth + "px",
        [position]: 0,
        width: width + "px",
        position: "absolute",
        height: height + "px",
        cursor: cursorMap[position],
        background: "transparent",
    });
    return edge;
}
function createAndAddEdgeElement(position) {
    const edge = createEdgeElement(position);
    content.appendChild(edge);
    edge.addEventListener("mousedown", (e) => {
        zoomDomMouseDown(e, position);
    });
    edge.addEventListener("touchstart", (e) => {
        zoomDomMouseDown(e, position);
    });
}
["top", "bottom", "left", "right"].forEach((position) => {
    createAndAddEdgeElement(position);
});

如下图黄色区域:

6、锚点元素点击开始事件

元素点击开始时,使用 getBoundingClientRect() 获取 content 元素的边界信息,并记录容器元素的初始位置及宽高。添加鼠标移动(触屏移动)和鼠标抬起(触屏结束)事件。

function zoomDomMouseDown(e, flag) {
  isMouseDown = flag;
  const bounding = content.getBoundingClientRect();
  defaultPos = {
      left: parseFloat(content.style.left || 0),
      top: parseFloat(content.style.top || 0),
      width: parseFloat(content.style.width || 0),
      height: parseFloat(content.style.height || 0),
      x: bounding.x,
      y: bounding.y,
      startX: e.clientX || e.touches[0].clientX,
      startY: e.clientY || e.touches[0].clientY,
  };
  document.addEventListener("mousemove", zoomDomMouseOver);
  document.addEventListener("mouseup", zoomDomMouseUp);
  document.addEventListener("touchmove", zoomDomMouseOver);
  document.addEventListener("touchend", zoomDomMouseUp);
}

7、鼠标移动事件监听

当用户在元素上按下鼠标或触摸屏幕时,首先检查 isMouseDown 变量,如果没有设置(即用户不是从锚点按下鼠标或触摸屏幕),则函数直接返回不执行任何操作。计算鼠标或触摸点相对于初始点击位置的移动距离(addWidth 和 addHeight)。根据用户拖动的方向(上、下、左、右),调整元素的大小和位置。如果用户从元素的边缘开始拖动,则相应地调整元素的大小。更新元素的样式,包括宽度、高度和位置。

边缘处理
  • 如果用户从顶部或底部拖动,调整元素的高度。
  • 如果用户从左侧或右侧拖动,调整元素的宽度。
  • 如果用户从边缘拖动,还需要调整元素的位置,确保元素不会超出其原始边界。
边界检查:

在调整大小和位置时,检查元素边界,以确保元素不会超出其预定的区域。

function zoomDomMouseOver(event) {
  if (!isMouseDown) return;
  const clientX = event.clientX || event.touches[0].clientX,
      clientY = event.clientY || event.touches[0].clientY;

  let addWidth = clientX - defaultPos.startX;
  let addHeight = clientY - defaultPos.startY;

  if (isMouseDown.includes("top")) {
      addHeight = -addHeight;
  }
  if (isMouseDown.includes("Left") || isMouseDown === "left") {
      addWidth = -addWidth;
  }

  const topMid = content.querySelector("#" + "topMid" + randomId);
  const leftMid = content.querySelector(
      "#" + "leftMid" + randomId
  );
  const rightMid = content.querySelector(
      "#" + "rightMid" + randomId
  );
  const bottomMid = content.querySelector(
      "#" + "bottomMid" + randomId
  );

  if (!["top", "bottom"].includes(isMouseDown)) {
      content.style.width = defaultPos.width + addWidth + "px";
      topMid.style.width =
          defaultPos.width + addWidth - rectWidth * 2 + "px";
      bottomMid.style.width =
          defaultPos.width + addWidth - rectWidth * 2 + "px";
  }
  if (!["left", "right"].includes(isMouseDown)) {
      content.style.height = defaultPos.height + addHeight + "px";
      leftMid.style.height =
          defaultPos.height + addHeight - rectWidth * 2 + "px";
      rightMid.style.height =
          defaultPos.height + addHeight - rectWidth * 2 + "px";
  }

  if (isMouseDown.includes("top")) {
      content.style.top =
          Math.min(
              defaultPos.top - addHeight,
              defaultPos.top + defaultPos.height
          ) + "px";
      if (
          defaultPos.top - addHeight >
          defaultPos.top + defaultPos.height
      ) {
          content.style.height = 0 + "px";
      }
  }
  if (isMouseDown.includes("Left") || isMouseDown === "left") {
      content.style.left =
          Math.min(
              defaultPos.left - addWidth,
              defaultPos.left + defaultPos.width
          ) + "px";
      if (
          defaultPos.left - addWidth >
          defaultPos.left + defaultPos.width
      ) {
          content.style.width = 0 + "px";
      }
  }
  if (isMouseDown.includes("bottom")) {
      content.style.top = defaultPos.top + "px";
  }
  if (isMouseDown.includes("Right")) {
      content.style.left = defaultPos.left + "px";
  }
}

8、鼠标抬起事件监听

当用户完成拖动操作,即松开鼠标按钮或停止触摸屏幕时,重置isMouseDown 变量并移除事件监听器。

function zoomDomMouseUp() {
    isMouseDown = "";
    document.removeEventListener("mousemove", zoomDomMouseOver);
    document.removeEventListener("mouseup", zoomDomMouseUp);
    document.addEventListener("touchmove", zoomDomMouseOver);
    document.addEventListener("touchend", zoomDomMouseUp);
}

完整代码

完整代码比较长,已上传到gitee,有需要的同学可以到这里查看:http://jyeontu.xyz/jvuewheel/#/JZoomView

组件库

组件文档

目前该组件也已经收录到我的组件库,组件文档地址如下:
http://jyeontu.xyz/jvuewheel/#/homePage

组件内容

组件库中还有许多好玩有趣的组件,如:

  • 悬浮按钮
  • 评论组件
  • 词云
  • 瀑布流照片容器
  • 视频动态封面
  • 3D轮播图
  • web桌宠
  • 贡献度面板
  • 拖拽上传
  • 自动补全输入框
  • 图片滑块验证

等等……

组件库源码

组件库已开源到gitee,有兴趣的也可以到这里看看:https://gitee.com/zheng_yongtao/jyeontu-component-warehouse

🌟觉得有帮助的可以点个star~

🖊有什么问题或错误可以指出,欢迎pr~

📬有什么想要实现的组件或想法可以联系我~

公众号

关注公众号『前端也能这么有趣』,获取更多有趣内容。

发送『组件库』获取源码

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

  • 29
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JYeontu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值