Open jtwang7 opened 2 years ago
参考文章:追求完美代码之——实现元素拖拽修改宽高和位移插件
点击期望修改的目标元素,显示该元素的边框大小,边框的角落有拖拽修改宽高的控件。
点击某个角落的拖拽控件,以该控件的的中心对称点为中心点,变更宽高。
newWidth = oldWidth + 控件 x 坐标变化量(可正可负) newHeight = oldHeight + 控件 y 坐标变化量(可正可负)
newWidth = oldWidth + 控件 x 坐标变化量(可正可负)
newHeight = oldHeight + 控件 y 坐标变化量(可正可负)
点击非某个角落的拖拽控件的拖拽控件,拖拽整个元素。
点击其他地方,控件消失,元素恢复原样。
mousedown
mousemove
存在的问题: 由于 mouse 事件是绑定在元素本身,mousedown ,mousemove , mouseup 都要求在元素上触发,这就导致假如鼠标移动过快时,元素还没有移动,鼠标就已经离开元素了,导致 mousemove 和 mouseup 无法有效触发,最终元素移动会显得卡顿,甚至会导致事件绑定无法及时清除。
mouseup
给顶部节点(如document)加上事件绑定,然后通过事件代理来实现拖拽元素准确定位:
// 拖拽元素dom const element = document.querySelector("div"); // 顶层节点绑定事件监听 document.addEventListener("mousedown", (e) => { // 过滤掉非目标元素 if (e.target !== element) { return; } let x0 = e.clientX; let y0 = e.clientY; const handleMove = ({ clientX, clientY, target }) => { element.style.left = `${parseFloat(element.style.left, 10) + clientX - x0}px`; element.style.top = `${parseFloat(element.style.top, 10) + clientY - y0}px`; x0 = clientX; y0 = clientY; }; document.addEventListener("mousemove", handleMove); const fn = () => { document.removeEventListener("mousemove", handleMove); document.removeEventListener("mouseup", fn); } document.addEventListener("mouseup", fn); });
控件的难点在于与实际目标元素的关联要时刻保持同步,这里利用了 Proxy 代理控件元素的样式修改,从而实现了修改控件元素的同时,修改目标元素的样式。
Proxy
function getPxNumber(str) { return parseFloat(str, 10); } // 目标元素dom const element = document.querySelector("div"); // 获取目标元素精确的初始位置 const { x, y, width, height } = element.getBoundingClientRect(); // 创建控件容器 const controlWrapper = document.createElement("div"); // 🌟 代理控件容器的style字段 // get: 访问控件容器style内部字段时,添加默认值逻辑判别 // set: 修改控件容器样式的同时同步更改目标元素的样式 const _style_ = new Proxy(controlWrapper.style, { get(target, key) { // 获取controlWrapper.style.xxx的初始样式值 let originalStyleValue = Reflect.get(target, key); // 添加默认值逻辑 if ( ["width", "height", "left", "top"].includes(key) && !originalStyleValue ) { // dom.style.xxx 没设置过是"",所以第一次获取时,要调用其他途径获取样式 originalStyleValue = controlWrapper.getBoundingClientRect()[key]; } return originalStyleValue; }, set(target, key, val) { const pxNumber = getPxNumber(val); // 修改控件容器时,同步将目标元素的style更改 if (["width", "height", "left", "top"].includes(key)) { element.style[key] = val; } Reflect.set(target, key, val); return val; }, }); // 将代理对象_style_挂载到控件容器dom对象上 // 注: dom.style 不可以覆盖,因此要挂载到新的自定义声明字段 _style_ controlWrapper._style_ = _style_; // 设置控件容器初始样式 Object.assign(controlWrapper.style, { position: "fixed", width: `${width}px`, height: `${height}px`, top: `${y}px`, left: `${x}px`, cursor: "all-scroll", border: "1px dashed #000", });
1️⃣ 创建控件元素
// 创建控件元素 function renderCorner({ width, height }) { // 来4个元素 const eles = Array.from({ length: 4 }).map(() => document.createElement("div") ); eles.forEach((x) => x.classList.add("controller-corner")); // 分别在topleft、topright、bottomleft、bottomright位置 const [tl, tr, bl, br] = eles; // 每一个角都移动半个身位 Object.assign(tl.style, { top: `-5px`, left: `-5px`, cursor: "nw-resize", }); Object.assign(tr.style, { top: `-5px`, cursor: "ne-resize", right: `-5px`, }); Object.assign(bl.style, { bottom: `-5px`, cursor: "sw-resize", left: `-5px`, }); Object.assign(br.style, { bottom: `-5px`, cursor: "se-resize", right: `-5px`, }); return { eles }; }
2️⃣ 抽离公共拖拽方法
// 抽离公共拖拽方法 // onMove:处理mousemove的函数 // bindUpAndDown:用来绑定up和down事件的,作为开始和收尾 function handleMouseDown(onMove, bindUpAndDown) { return function ({ target, clientX: x, clientY: y }) { let x0 = x; let y0 = y; function handleMove(e, ...rest) { const { clientX, clientY } = e; e.preventDefault(); const detaX = clientX - x0; const detaY = clientY - y0; x0 = clientX; y0 = clientY; // 我们前面说到,拖拽过程中,x、y坐标变化量是核心的参数 onMove(target, detaX, detaY, ...rest); } // 透传target和handleMove,因为开始和收尾的down和up都要用到它们 bindUpAndDown(target, handleMove); }; }
3️⃣ 添加功能
// 获取四个角——eles,传入的width, height是目标元素的getBoundingClientRect const { eles } = renderCorner({ width, height }); const [tl, tr, bl, br] = eles; // 在handleMouseDown传入onMove, bindUpAndDown const handleControlerMouseDown = handleMouseDown( (target, detaX, detaY, isMoveTargetElement) => { // 移动的时候的处理 // 是否是左边两个角 const isLeft = [tl, bl].includes(target); // 是否是上面两个角 const isTop = [tl, tr].includes(target); // 在左边,deta变化量要相反 const directionLeft = !isLeft ? 1 : -1; const directionTop = !isTop ? 1 : -1; // 新的宽度、高度 let newWidth = getPxNumber(ele._style_.width) + directionLeft * detaX; let newHeight = getPxNumber(ele._style_.height) + directionTop * detaY; // 区分拖动非4个角的控件的情况,此时是拖动整个元素本身 if (isMoveTargetElement) { const newL = getPxNumber(ele._style_.left); const newT = getPxNumber(ele._style_.top); ele._style_.left = `${newL + detaX}px`; ele._style_.top = `${newT + detaY}px`; return; } // 拖动4个角 ele._style_.width = `${newWidth}px`; ele._style_.height = `${newHeight}px`; // 拖左边的时候,实际上也会拖动元素本身 ele._style_.left = isLeft ? `${getPxNumber(ele._style_.left) - directionLeft * detaX}px` : ele._style_.left; ele._style_.top = isTop ? `${getPxNumber(ele._style_.top) - directionTop * detaY}px` : ele._style_.top; }, (target, handleMove) => { // 绑定事件的时候的处理 const handleMoveTargetElement = (e) => handleMove(e, true); // 针对拖动4个角和非4个角的处理 // 拖4个角改变宽高 if (eles.includes(target)) { document.addEventListener("mousemove", handleMove); } else { // 拖控件非4个角的本体部分改变位置 document.addEventListener("mousemove", handleMoveTargetElement); } document.addEventListener("mouseup", ({ target }) => { document.removeEventListener("mousemove", handleMove); document.removeEventListener("mousemove", handleMoveTargetElement); }); } ); document.addEventListener("mousedown", handleControlerMouseDown); // 挂载元素 eles.forEach((e) => { ele.appendChild(e); });
4️⃣ 支持随时移除、增加控件
// 在挂载元素后,return一个清除事件的方法 eles.forEach((e) => { ele.appendChild(e); }); return { removeControler() { eles.forEach((e) => { ele.removeChild(e); }); document.removeEventListener("mousedown", handleControlerMouseDown); }, eles: [...eles, ele], };
🔆 总入口
function injectDragger(ele) { let removeDragger; ele.addEventListener("click", () => { if (!removeDragger) { // 增加控件,然后保存暴露出来的清除方法随时使用 const { removeAllControler, eles } = injectController(ele); removeDragger = removeAllControler; const handleRemove = ({ target }) => { // 监听鼠标弹起,如果不是从控件容器弹起,也就是点了其他地方,那这些控件都要删掉 if (![...eles, ele].includes(target)) { removeDragger && removeDragger(); removeDragger = undefined; document.removeEventListener("mouseup", handleRemove); } }; document.addEventListener("mouseup", handleRemove); } }); }
实现元素拖拽修改宽高和位移
参考文章:追求完美代码之——实现元素拖拽修改宽高和位移插件
❇️ 拖拽修改的基本流程
点击期望修改的目标元素,显示该元素的边框大小,边框的角落有拖拽修改宽高的控件。
点击某个角落的拖拽控件,以该控件的的中心对称点为中心点,变更宽高。
点击非某个角落的拖拽控件的拖拽控件,拖拽整个元素。
点击其他地方,控件消失,元素恢复原样。
❇️ 拖拽实现
❎ 错误实现
mousedown
事件,按键按下时绑定mousemove
。mousemove
事件触发的时候,计算本次位置和上次位置 x、y 坐标(即left、top)差值,并加上 left、top 位置,即可获得拖动后的新位置mousemove
事件绑定。存在的问题: 由于 mouse 事件是绑定在元素本身,
mousedown
,mousemove
,mouseup
都要求在元素上触发,这就导致假如鼠标移动过快时,元素还没有移动,鼠标就已经离开元素了,导致mousemove
和mouseup
无法有效触发,最终元素移动会显得卡顿,甚至会导致事件绑定无法及时清除。✅ 正确实现
给顶部节点(如document)加上事件绑定,然后通过事件代理来实现拖拽元素准确定位:
❇️ 控件实现
❎ 难点
控件的难点在于与实际目标元素的关联要时刻保持同步,这里利用了
Proxy
代理控件元素的样式修改,从而实现了修改控件元素的同时,修改目标元素的样式。💡Proxy 代理样式修改!
✅ 控件实现
1️⃣ 创建控件元素
2️⃣ 抽离公共拖拽方法
3️⃣ 添加功能
4️⃣ 支持随时移除、增加控件
🔆 总入口