9

可视化搭建平台之跨iframe拖拽

 3 years ago
source link: https://zhuanlan.zhihu.com/p/353043053
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

可视化搭建平台之跨iframe拖拽

关注微信公众号:大转转FE。 一个有趣的前端团队~

前段时间做运营活动搭建平台,其中一个主要功能:编辑页面分为左侧-组件区与右侧-预览区,需要实现组件区的内容可自由放置到预览区内。

类似下图所示:

v2-78e1a1d63b820ea1f0d1cac0c1cd536b_720w.jpg

社区内有一些类似的功能实现,但使用的方式大同小异,都离不开拖拽能力。我们日常开发中会经常用到的拖拽,如拖拽排序,拖拽上传等。当然拖拽的 npm 包也有很多,比较好用的包有 react-dnd, vue 自带的拖拽能力等。

但我们的预览区采用的是 iframe 方式,社区好用的类库一般不支持跨 iframe 的拖拽的能力。此处我们选择了使用原生拖拽 drag 和 dropAPI

需要实现的主要功能,有两点:

1、检测拖动到 iframe 内部和外部。

2、数据驱动来进行 iframe 内部组件的展示。

我们简单生成页面的功能:

//搭建编辑页
//drag.jsx
import React, { useState, useEffect } from 'react';
import Drag from './drag.js';

require('./styles.less');

//iframe hooks
const useIframeLoad = () => {
  const [iframeState, setIframeState] = useState(false);
  const [windowState, setWindowState] = useState( document.readyState === "complete");

  const iframeLoad = () => {
    const iframeEle = document.getElementById("my-iframe");
    iframeEle && setIframeState(iframeEle.contentDocument.readyState === "complete");
    if (!iframeState && iframeEle) {
      iframeEle.onload = () => {
        setIframeState(true);
      };
    }
  };
  useEffect(() => {
    if (!windowState) {
      setIframeState(false);
      window.addEventListener('load', () => {
        setWindowState(true);
        iframeLoad();
      })
    } else {
      iframeLoad();
    }
  }, []);
  return iframeState;
}

export default () => {

  const init = () => {
    Drag.init({
      dragEle: document.getElementById('drag-box'),
      dropEle: document.getElementById('my-iframe').contentDocument.getElementById('drop-box')
    })
  }

  useIframeLoad() && init();

  return <>
   <!-- 组件区 -->
    <div id="drag-box">
      <div className="drag-item">拖动元素</div>
      <div className="drag-item">拖动元素</div>
      <div className="drag-item">拖动元素</div>
    </div>
    <!-- 预览区 -->
    <div className="drop-content">
      <iframe id="my-iframe" src="#/iframe" style={{ width: "100%", height: "480px", border: "none" }}/>
    </div>
  </>
}

预览区 iframe 页:

//iframe.jsx
import React from 'react';

require('./styles.less');

export default () => {
  return <div id="drop-box">
    <div className="item">元素1</div>
    <div className="item">元素2</div>
    <div className="item">元素3</div>
  </div>
}

此时,简单的搭建编辑布局已完成。接下来,我们看下拖拽部分:

跨 iframe 拖拽

首先我们可以看下有哪些原生事件

drag // 拖动元素或文本选择时将触发此事件 (相当于拖动过程中,一直触发此事件)
dragstart //当用户开始拖动一个元素或者一个选择文本的时候 ,将触发此事件
dragend //当拖动操作结束时(通过释放鼠标按钮或按退出键),将触发此事件

dragover //当被拖动元素在释放区内移动时,将触发此事件
dragenter //被拖动元素进入到释放区所占据得屏幕空间时,将触发此事件
dragleave //当被拖动元素没有放下就离开释放区时,将触发此事件
dragexit //当元素不再是拖动操作的立即选择目标时,将触发此事件
drop //当被拖动元素在释放区里放下时,将触发此事件

原生 drag 和 drop 拖拽

基于需求,拆分出拖拽的关键流程:

  • 初始化元素 设置拖动元素和目标节点
  • 注册事件 对拖动元素和目标节点元素注册 drag 事件
  • 监听事件 拖动过程中生成占位节点,拖动结束删除此占位节点

不完全代码如下:

//drag.js
class Drag {
  params = {}

  init = (params) => {
    ....
  };

  //初始化设置拖动元素
  initDrag = dragEle => {
    if(dragEle.childNodes.length) {
      const { length } = dragEle.childNodes;
      let i = 0
      while (i< length) {
        this.setDrag(dragEle.childNodes[i]);
        i += 1;
      }
    } else {
      this.setDrag(dragEle);
    }
  }

  //初始化释放区
  initDrop = dropEle => {
    if (dropEle.childNodes.length) {
      const { length } = dropEle.childNodes;
      let i = 0;
      while (i < length) {
        this.setDrop(dropEle.childNodes[i]);
        i += 1;
      }
    } else {
      this.setDrop(dropEle);
    }
  }

  //拖动元素注册事件
  setDrag = el => {
    el.setAttribute("draggable", "true");
    el.ondragstart = this.dragStartEvent;
    el.ondrag = this.dragEvent;
    el.ondragend = this.dragEndEvent;
  };

  //释放区注册事件
  setDrop = el => {
    el.ondrop = this.dropEvent;
    el.ondragenter = this.dragEnterEvent;
    el.ondragover = this.dragOverEvent;
    el.ondragleave = this.dragLeaveEvent;
  }
   ......

  //创建占位元素
  createElePlaceholder = (() => {
    let ele = null;
    return () => {
      if (!ele) {
        ele = document.createElement("div");
        ele.setAttribute("id", "drag-ele-placeholder");
        ele.innerHTML = `<div style="width: 100%; height:50px; position: relative">
            <div style="width: 150px; height: 40px; text-align: center; position: absolute;
            left: 0; right: 0; top: 0; bottom:0; margin: auto; background: #878; line-height: 40px">放置组件</div>
          </div>`;
      }
      return ele;
    };
  })();

  //移除占位元素
  removePlaceholderEle = () => {
    const iframe = this.getIframe();
    const removeEle = iframe.contentDocument.getElementById("drag-ele-placeholder");
    const { dropEle } = this.params;
    if(this.isHasPlaceholderEle()) { dropEle.removeChild(removeEle) };
  }

  /****** 事件处理 ******/
  dragEndEvent = ev => {
    this.removePlaceholderEle()
    console.log('拖拽结束');
    console.log('删除占位元素');
  };
  //插入占位元素
  dragEnterEvent = ev => {
    ev.preventDefault();
    const insertEle = this.createElePlaceholder();
    ev.target.before(insertEle);
    console.log('进入到可放置区');
    console.log('插入占位元素');
  };
   //删除占位元素
  dragLeaveEvent = ev => {
    ev.preventDefault();
    this.removePlaceholderEle()
    console.log('离开放置区');
    console.log('删除占位元素');
  };

  dropEvent = ev => {
    ev.preventDefault();
    console.log('在放置区放开鼠标');
  }
}

export default new Drag();

初步完成后,效果如下:

v2-68e5c9a45800cb913e2ec4e5967b3da0_b.jpg

此处存在一些问题:

  1. 在插入时,页面闪烁
  2. 只有鼠标位置进入释放区,才触发进入事件
  3. 无法实现第一个元素的添加

问题分析

  • 当拖到预览区时,会触发预览区内的节点 dragenter 事件。每当在当前节点上插入占位元素时,此节点的位置会发生变化,触发节点 dragleave 事件,同时删除占位元素。此过程一直重复,导致一直闪烁。
  • 上述 2,3 问题,是由于 drag/drop 本身 api 限制

由于现在的方式无法真正完美的实现功能,决定弃用 dragover,dragenter,dragleave 事件

重新梳理需要优化的功能点:

  1. 当拖动元素和 iframe 的边有接触的时候,就代表进入释放区
  2. 拖动可以实现元素上面插入,和元素下面插入

使用坐标精准计算,来处理进入释放区和在元素上面和下面插入

对 drag.js 做些改造:

class Drag {
  params = {}

  // 声明
  mouseOffsetBottom = 0;
  mouseOffsetRight = 0;

  init = (params) => {
    ...
  };
  //初始化设置拖动元素
  initDrag = dragEle => {
    ....
  }
  //初始化释放区
  initDrop = dropEle => {
    ...
  }
  //拖动元素注册事件
  setDrag = el => {
    ...
  };
  //释放区注册事件
  setDrop = el => {
    ...
  }

  //获取iframe的位置
  getIframeOffset = () => {
    const iframeEle = this.getIframe();
    return iframeEle
      ? this.getRealOffset(iframeEle)
      : { offsetLeft: 0, offsetTop: 0 };
  };

  //递归计算元素距离父元素的offset
  getRealOffset = (el, parentName) => {
    let left = el.offsetLeft;
    let top = el.offsetTop;
    if (el.offsetParent && el.offsetParent.tagName !== parentName) {
      const p = this.getRealOffset(el.offsetParent, parentName);
      left += p.offsetLeft;
      top += p.offsetTop;
    }
    return { offsetLeft: left, offsetTop: top };
  }

  //获取元素位置
  getElOffset = el => {
    const { offsetTop: iframeTop } = this.getIframeOffset();
    const { offsetTop: targetOffsetTop } = this.getRealOffset(el);
    return {
      midLine: el.clientHeight / 2 + targetOffsetTop + iframeTop,
      topLine: targetOffsetTop + iframeTop,
      bottomLine: el.clientHeight + targetOffsetTop + iframeTop
    };
  };

  //释放区内部元素位置
  getDropOffset = () => {
    const result = [];
    const { dropEle } = this.params;
    const el = dropEle.childNodes;

    let i = 0;
    while (i < el.length) {
      const midLine = this.getElOffset(el[i]);
      result.push(midLine);
      i += 1;
    }
    return result;
  };

  //位置比较
  locationCompare = (ev) => {
    let inside = false;
    const { dropEle } = this.params;
    console.log(ev.clientX);
    // 拖动元素的位置
    const sourceRight = ev.clientX + this.mouseOffsetRight;
    const sourceLeft = sourceRight - ev.currentTarget.clientWidth;

    const { offsetLeft: iframeLeft } = this.getIframeOffset();
    const { offsetLeft: targetLeft } = this.getRealOffset(dropEle);

    /*释放区的位置*/
    const targetOffsetLeft = iframeLeft + targetLeft;
    const targetOffsetRight = targetOffsetLeft + dropEle.clientWidth;

    if (sourceRight > targetOffsetLeft && sourceLeft < targetOffsetRight) {
      //拖动到释放区
      inside = true;
    } else {
      //释放区外面
      inside = false;
    }
    return inside;

  }

  //插入占位元素
  insertPlaceholderEle = (sourceMidLine) => {
    const dropOffset = this.getDropOffset(); //释放区的位置属性
    const insertEl = this.createElePlaceholder();
    const { dropEle } = this.params;
    const dropEleChild = dropEle.childNodes;
    if (dropOffset.length) {
      dropOffset.map((item, i) => {
        const Ele = dropEleChild[i];
        //在元素前面插入占位元素
        if (sourceMidLine > item.topLine && sourceMidLine < item.midLine) {
          Ele.before(insertEl);
        }
        //在元素后面插入占位元素
        if (sourceMidLine < item.bottomLine && sourceMidLine > item.midLine) {
          this.index = i + 1;
          Ele.after(insertEl);
        }
        //追加一个占位元素
        if (sourceMidLine > dropOffset[dropOffset.length - 1].bottomLine) {
          dropEle.append(insertEl);
        }
        return item;
      });
    }
    //插入第一个占位元素(当iframe内部没有组件)
    if (!dropEleChild.length) {
      dropEle.append(insertEl);
    }
  }

  /****** 事件处理 ******/
  dragStartEvent = ev => {
    // console.log('开始拖拽');
    //获得鼠标距离拖拽元素的下边的距离
    this.mouseOffsetBottom = ev.currentTarget.clientHeight - ev.offsetY;
    //获得鼠标距离拖拽元素的右边的距离
    this.mouseOffsetRight = ev.currentTarget.clientWidth - ev.offsetX;
  };

  dragEvent = ev => {
    //获取拖拽元素中线距离屏幕上方的距离
    const sourceMidLine =
      ev.clientY + this.mouseOffsetBottom - ev.currentTarget.clientHeight / 2;
    if(this.locationCompare(ev)) {
      this.insertPlaceholderEle(sourceMidLine)
      console.log('释放区内部')
    } else {
      this.removePlaceholderEle()
      console.log('释放区外面')
    }
  };
}

export default new Drag();

生成结果如下:

v2-f01647376acf95a8fc87b8240a73cb8a_b.jpg

此时已经解决了不停闪烁的问题,以及精准坐标计算,实现元素的上下插入。

但是还是存在一些问题:

  • 演示图中可以明显看到,拖动元素右边刚进入 iframe 的时候,可以插入占位元素,但是等到鼠标位置进入 iframe 的时候,就会又删除了元素

这是什么原因呢?

我们看一下打印的鼠标的坐标,可以看到鼠标位置进入 iframe 的时候,ev.clientX 突变成 0,由此可见,鼠标坐标进入 iframe 的时候,就以 iframe 为窗口了。导致鼠标的位置突变成 0,就导致计算位置出现偏差,从而拖拽元素被认为不在释放区内,所以就删除了占位元素。

怎么解决这个问题呢?

想到了几个方案:

  1. 一个是监听坐标的突变情况,然后重新计算位置,进一步进行比较位置。
  2. 把 iframe 放大和屏幕大于等于屏幕的大小,从拖动开始就使得在 iframe 里面。

方案分析:

  • 第一个方案,监听坐标突变为 0 这个临界条件不靠谱,因为每隔 50ms 拖动事件才触发,根据你移动鼠标的快慢,每次鼠标进入 iframe 获取的 clientX 不一致,第一种方案不可行
  • 第二个方案,iframe 放大,理论上是可以的,我们来试试。主要是改变布局

代码如下:

.drop-content {
  position: absolute;
  width: 100vw; //iframe放大和窗口一般大
  height: 100%;
}

#drop-box {
  width: 375px; //iframe内部元素设置宽度
  margin: 100px auto;

  .item {
    ...
  }
}

演示效果如下

v2-e1de3f6b37c8dee4e2e5956778abc973_b.jpg

演示可以看到,覆盖了左边的组件区。这是由于右边视图区 z-index 比较高导致的。

有两个方案

  • 元素布局移动调换位置,让右边视图区 dom 元素放在组件区的前边。
  • 更改 z-index,让右边视图区的 z-index 低一点
//drag.jsx
//调换两个元素的位置
<>
  <div className="drop-content">
    <iframe id="my-iframe" src="#/iframe" style={{ width: "100%", height: "480px", border: "none" }}/>
  </div>
  <div id="drag-box">
    <div className="drag-item">拖动元素</div>
    <div className="drag-item">拖动元素</div>
    <div className="drag-item">拖动元素</div>
  </div>
</>

实现后的效果

v2-89dc394839eab28155e94e2c36040f2c_b.jpg

可以看出来,完美解决了拖动的问题。但是就是对布局进行了改变。

.drop-content {
  position: absolute;
  z-index: -1; //让iframe的z-index低一点
  width: 100vw; //iframe放大和窗口一般大
  height: 100%;
}

#drop-box {
  width: 375px; //iframe内部元素设置宽度
  margin: 100px auto;

  .item {
    width: 100%;
    height: 50px;
    background-color: #875;
  }
}

实现后的效果

v2-b0b2635f05831d62b4372ae4b0dc420d_b.jpg

演示中可以看出来,拖拽的问题完美解决,但是 iframe 的里面元素点击事件没有触发。

想了想,既然 z-index 可以解决 clientX 的突变问题,那是不是可以不用放大 iframe 来做?这样也会不影响事件的触发,那我们试试吧。

//drag.js
//开始拖拽
dragStartEvent = ev => {
  document.getElementsByClassName("drop-content")[0].style.zIndex =
    "-1";
};

//拖拽结束
dragEndEvent = ev => {
  ev.preventDefault();
  document.getElementsByClassName("drop-content")[0].style.zIndex = "0";
};

演示效果如下

v2-7f86b7928e89b8926040382246fdc181_b.jpg

很好,这样也可以完美解决拖动的问题,而且不用改变 dom 的位置。

当视图区元素比较多,页面出现滚动条时,会不会出现问题呢?我们试着把 iframe 的高度写高一点

<iframe id="my-iframe" src="#/iframe" style={{ width: "100%", height: "880px", border: "none" }}/>

演示效果如下

v2-88eed7a35671982eb2b52eddd9af28fc_b.jpg

演示中可以看出来,页面出现滚动条,视图区滚动上去,iframe 顶部滚入到屏幕顶部的时候,我们来拖动元素插入的时候,就会出现,错位插入,这是计算又出了问题?

仔细看看代码,iframe 顶部滚入到屏幕顶部的时候,就会出现计算出负数的情况,导致计算偏差,从而导致插入占位元素错位。

//递归计算元素距离父元素的offset
getRealOffset = (el, parentName) => {
  let left = el.offsetLeft;
  let top = el.offsetTop;
  if (el.offsetParent && el.offsetParent.tagName !== parentName) {
    const p = this.getRealOffset(el.offsetParent, parentName);
    left += p.offsetLeft;
    top += p.offsetTop;
  }
  return { offsetLeft: left, offsetTop: top };
}

优化计算方案

//计算元素距离父元素的offset
getRealOffset = (el, parentName) => {
  const { left, top } = el.getBoundingClientRect();
  return { offsetLeft: left, offsetTop: top };
}

使用 getBoundingClientRect 这个方法获得具体窗口的位置

v2-d5b0444b4e1d61d61ebd73710ce9ffc9_b.jpg

本次优化,可以很完美的解决了拖动的一些问题,以上两种方案都是行的。

跨 iframe 通信

如何在拖动元素插入之后,让 iframe 内部的数据也实时更新渲染呢?

思路如下:

  • iframe 内挂载一个 update 方法
  • 在拖动完成后的回调里面,调用 update,传入数据
  • 触发 iframe 内部元素的渲染
  1. 维护一个组件的数据 store,getStore,和 setStore方法
//store.js
class Store {
  state = {
    list: []
  }
  getStore = () => this.state
  setStore = (data) => {
    this.state = { ...this.state, ...data }
  }
}

export default new Store()

2. 组件的插入对应数据的处理,包含,add 和 insert操作,以及同步更新 iframe的方法

// update.js
import Store from './store';

const add = (params) => {
  const { list } = Store.getStore()
  Store.setStore({ list: [...list, params.data]})
};

const insert = (params) => {
  const { list } = Store.getStore()
  const { index } = params;
  list.splice(index, 0, params.data)
  Store.setStore({ list: [...list] })
};

const update = {
  add,
  insert
}

//更新iframe内部数据方法
const iframeUpdate = (params) => {
  document.getElementById("my-iframe") &&
    document.getElementById("my-iframe").contentWindow &&
    document.getElementById("my-iframe").contentWindow.update &&
    document.getElementById("my-iframe").contentWindow.update(params);
}

export default (params) => {
  const { type, ...argv } = params;
  if(!type) return Promise.reject()
  return new Promise(r => r())
    .then(() => update[type](argv))
    .then(() => {
      const { list } = Store.getStore()
      iframeUpdate(list)
    })
}

3. 拖动的时候,拖动完毕后,将元素的操作类型,以及要插入的元素的位置,通过回调函数传递出去

//drag.js
class Drag {
  params = {}

  mouseOffsetBottom = 0;
  mouseOffsetRight = 0;

  index = 0; //插入元素的下标
  type = 'add'; //操作类型

  init = (params) => {
    ...
  };

  ...

  //计算元素距离父元素的offset
  getRealOffset = (el, parentName) => {
    const { left, top } = el.getBoundingClientRect();
    return { offsetLeft: left, offsetTop: top };
  }

  //获取元素位置
  getElOffset = el => {
    const { offsetTop: iframeTop } = this.getIframeOffset();
    const { offsetTop: targetOffsetTop } = this.getRealOffset(el);
    return {
      midLine: el.clientHeight / 2 + targetOffsetTop + iframeTop,
      topLine: targetOffsetTop + iframeTop,
      bottomLine: el.clientHeight + targetOffsetTop + iframeTop
    };
  };

  //释放区内部元素位置
  getDropOffset = () => {
    const result = [];
    const { dropEle } = this.params;
    const el = dropEle.childNodes;

    let i = 0;
    while (i < el.length) {
      const midLine = this.getElOffset(el[i]);
      result.push(midLine);
      i += 1;
    }
    return result;
  };

  ...

  //插入占位元素
  insertPlaceholderEle = (sourceMidLine) => {
    const dropOffset = this.getDropOffset(); //释放区的位置属性
    const insertEl = this.createElePlaceholder();
    const { dropEle } = this.params;
    const dropEleChild = dropEle.childNodes;
    if (dropOffset.length) {
      dropOffset.map((item, i) => {
        const Ele = dropEleChild[i];
        //在元素前面插入占位元素
        if (sourceMidLine > item.topLine && sourceMidLine < item.midLine) {
          Ele.before(insertEl);
          this.index = i;
          this.type = 'insert'
        }
        //在元素后面插入占位元素
        if (sourceMidLine < item.bottomLine && sourceMidLine > item.midLine) {
          this.index = i + 1;
          Ele.after(insertEl);
          this.type = 'insert'
        }
        //追加一个占位元素
        if (sourceMidLine > dropOffset[dropOffset.length - 1].bottomLine) {
          dropEle.append(insertEl);
          this.type = 'add'
        }
        return item;
      });
    }
    //插入第一个占位元素(当iframe内部没有组件)
    if (!dropEleChild.length) {
      this.type = 'add'
      dropEle.append(insertEl);
    }
  }

  /****** 事件处理 ******/
  //开始拖拽
  dragStartEvent = ev => {
    document.getElementsByClassName("drop-content")[0].style.zIndex =
      "-1";
    //获得鼠标距离拖拽元素的下边的距离
    this.mouseOffsetBottom = ev.currentTarget.clientHeight - ev.offsetY;
    //获得鼠标距离拖拽元素的右边的距离
    this.mouseOffsetRight = ev.currentTarget.clientWidth - ev.offsetX;
  };

  dragEvent = ev => {
    //获取拖拽元素中线距离屏幕上方的距离
    const sourceMidLine =
      ev.clientY + this.mouseOffsetBottom - ev.currentTarget.clientHeight / 2;
    if(this.locationCompare(ev)) {
      this.insertPlaceholderEle(sourceMidLine)
      // console.log('释放区内部')
    } else {
      this.removePlaceholderEle()
      // console.log('释放区外面')
    }
  };

  //拖拽结束
  dragEndEvent = ev => {
    ev.preventDefault();
    document.getElementsByClassName("drop-content")[0].style.zIndex = "0";
    const { callback } = this.params;
    this.locationCompare(ev) &&
      callback &&
      callback({
        type: this.type,
        index: this.index
      });
  };
}

export default new Drag();

4. 在拖动完毕后调用 update,更新数据源

//drag.jsx
import React, { useState, useEffect } from 'react';
import Drag from './drag';
import update from '@/store/update';

require('./styles.less');

//iframe hooks
const useIframeLoad = () => {
  ...
  //iframe加载状态的hooks
  return iframeState;
}

export default () => {

  const callback = params => {
    update({ ...params, data: { name: new Date().getTime() } })
  }

  const init = () => {
    Drag.init({
      dragEle: document.getElementById('drag-box'),
      dropEle: document.getElementById('my-iframe').contentDocument.getElementById('drop-box'),
      callback
    })
  }

  useIframeLoad() && init();
  return <>
    ...
  </>
}

5. iframe 内部 update 方法被调用,就会触发数据更新和组件的渲染。

//iframe.jsx
import React, { useState } from 'react';

require('./styles.less');

export default () => {
  const [list, setList] = useState([]);

  //挂载update方法,跨iframe数据传递,更新
  window.update = params => {
    setList(params);
  }

  return <div id="drop-box">
    {
      list.map((item) =>
        <div className="item" key={item.name} onClick={() => alert('点击事件')}>元素{item.name}</div>
      )
    }
  </div>
}

演示效果如下

v2-1049c1dc572633367a642362c1fb7a31_b.jpg

最终实现了跨 iframe 的拖拽及通信。

此次运营页搭建拖拽通信功能,是在不断打怪升级中完成。其中涉及到以下几个点:

  • 元素进入视图区的判断 iframe 的左边距离屏幕的 x 的坐标 < 被拖元素的右边距离屏幕的 x 的坐标 < iframe 的右边距离屏幕的 x 的坐标。
  • 元素上下插入 被拖元素的中线距离屏幕的 y 的坐标 < iframe 内部元素中线距离屏幕的 y 的坐标 属于前面插入,被拖元素的中线距离屏幕的 y 的坐标 > iframe 内部元素中线距离屏幕的 y 的坐标 属于后面插入。
  • clientX 坐标突变的问题 z-index 解决处理。
  • 滚动位置问题 getBoundingClientRect 解决。

希望本篇文章对你有所帮助,欢迎大家一起交流分享呀。

每天进步一点点,质变从关注 大转转 FE 开始。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK