实现目标效果
前言
在这之前,推荐大家先看一下上一篇文章 js 元素可随意拖动变形 这篇实现了,具体实现了大部分功能,这个标线就是在那基础上增加的,但是并没有增加标线吸附效果。
1. 实现原理
在getClientRect方法中可以获取到想要的数据“left,right,top,bottom”,在这个基础上可以进行双层for循环
第一次for循环循环小盒子,并判断是不是当前元素,是的话跳过循环
第二次for循环arr数组获取itemInfo中的数据
第三次for循环arr数组获取this.currentEl中的数据
在判断中需要增加一个限制,例如:top:100 == left:100这样的标线出现的位置就会错乱出现在left,增加的限制具体判断这两个属性是否在同一轴上,不是同一轴的跳过判断,不创建标线,可以看RestrictionDirection方法
// 判断是否出现标线 isShowLine(){ const arr = ['bottom','left','top','right']; const rect = this.getClientRect(this.currentEl); for(let i = 0; i < this.itemList.length; i++){ const item = this.itemList[i]; const itemInfo = this.getClientRect(item); if(this.currentEl === item) continue; for(let j = 0; j < arr.length; j++){ for(let k = 0; k < arr.length; k++){ if(this.RestrictionDirection(arr[k],arr[j]) && rect[arr[k]] == itemInfo[arr[j]]){ this.createShowLine(arr[j],itemInfo[arr[j]]); } } } } } // 限制方向 RestrictionDirection(type1,type2){ const arr = [['bottom','top'],['left','right']]; for(let i = 0; i < arr.length; i++){ if(arr[i].includes(type1) && arr[i].includes(type2)){ return true; }else if(arr[i].includes(type2) && arr[i].includes(type1)){ return true; } } } createShowLine(type,num){ // type 方向 num 位置 const arr1 = ['top','bottom']; const arr2 = ['left','right']; const line = document.createElement('div'); line.id = 'drag-line'; line.style.cssText = ` position: absolute; width: ${arr1.includes(type) ? '100%' : '1px'}; height: ${arr2.includes(type) ? '100%' : '1px'}; background-color: ${this.markingColor}; ${arr1.includes(type) ? 'top' : 'left'}: ${num}px; }`; this.el.appendChild(line); }
3. 标线出现的时间和消失的时间
标线出现的时间,我这里设置的是在移动时或顶点拉动元素时,
标线消失的时间,我这里设置的是小盒子鼠标抬起时和顶点鼠标抬起时
4. 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="./drag.js"></script>
<title>js 拖动元素(标线)</title>
<style>
*{
margin: 0;
padding: 0;
}
.container{
width: 600px;
height: 600px;
background-color: aqua;
}
.drag-item{
width: 100px;
height: 100px;
cursor: move;
z-index: 1000;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<div class="container">
<div class="drag-item" style="background-color: blue;">1</div>
<div class="drag-item" style="background-color: red;">2</div>
<div class="drag-item" style="background-color: green;">3</div>
</div>
<button onclick="getData()">获取JSON数据</button>
<script>
class Drag{
constructor({
el,item,marking,markingColor,dashed,dashedColor
}){
this.el = this.$(el);
this.item = item;
this.itemList = this.$All(item);
this.itemBoxSize = 5;
this.marking = marking;
this.markingColor = markingColor;
this.dashed = dashed;
this.dashedColor = dashedColor;
this.InitPointer = {x:0,y:0}; // 鼠标初始位置
this.currentEl = null;
this.currentElItem = null;
this.isActive = false; // 标记是否第一次点击
this.zIndex = 2000;
this.init();
}
init(){
this.initEl();
this.initItemEl();
this.addEventEl();
this.resetShowItem();
}
initEl(){
this.el.style.cssText = 'position: relative;';
}
addEventEl(){
document.addEventListener('mousedown',this.windowMouseDown);
for(let i = 0; i < this.itemList.length; i++){
this.itemList[i].addEventListener('mousedown',this.ELMouseDown);
}
}
// 处理点击空白区域方法
windowMouseDown = (e) => {
this.resetShowItem();
this.removeDashed();
}
ELMouseDown = (e) => {
// 阻止向下冒泡
e.stopPropagation();
if(this.currentEl && e.target.className !== 'drag-item-box' && this.currentEl != e.target){
this.resetShowItem();
this.removeDashed();
}
this.currentEl = e.target;
// 设置this.currentEL的z-index属性
this.currentEl.style.zIndex = this.zIndex++;
let rect = this.getClientRect(this.currentEl);
this.elInitPointer = {x:e.x,y:e.y,rectX:rect.left,rectY:rect.top,rectW:rect.width,rectH:rect.height};
this.currentElItem = e.target;
this.showCurrentItem();
// 区分方块
if(e.target.className == 'drag-item-box'){
this.isActive = true
this.currentEl = e.target.offsetParent
rect = this.getClientRect(this.currentEl);
this.elInitPointer = {x:e.x,y:e.y,rectX:rect.left,rectY:rect.top,rectW:rect.width,rectH:rect.height,top:rect.top,left:rect.left};
document.addEventListener('mousemove',this.itemMouseMove);
document.addEventListener('mouseup',this.itemMouseUp);
return;
}
this.createDashed();
document.addEventListener('mousemove',this.ELMouseMove);
document.addEventListener('mouseup',this.ELMouseUp);
}
ELMouseMove = (e) => {
this.removeShowLine();
const {x:initX,y:initY,rectX,rectY} = this.elInitPointer;
const {x,y} = e;
const xMove = rectX + x - initX;
const yMove = rectY + y - initY;
this.currentEl.style.top = `${yMove}px`;
this.currentEl.style.left = `${xMove}px`;
this.isOutBoundary();
this.isShowLine();
}
ELMouseUp = (e) => {
document.removeEventListener('mousemove',this.ELMouseMove);
document.removeEventListener('mouseup',this.ELMouseUp);
this.removeShowLine();
}
itemMouseMove = (e) => {
this.removeShowLine();
this.initItemBox(e);
this.isShowLine()
}
itemMouseUp = () => {
document.removeEventListener('mousemove',this.itemMouseMove);
document.removeEventListener('mouseup',this.itemMouseUp);
this.removeShowLine();
}
// 当前元素顶点显示
showCurrentItem(){
const list = this.currentEl.querySelectorAll('.drag-item-box');
if(list.length === 0) return;
for(let i = 0; i < list.length; i++){
list[i].style.display = 'block';
}
}
// 重置所有顶点显示
resetShowItem(){
const list = this.$All('.drag-item-box');
if(list.length === 0) return;
for(let i = 0; i < list.length; i++){
list[i].style.display = 'none';
}
}
createDashed(){
if(this.dashed && this.currentEl){
this.currentEl.style.outline = '1px dashed ' + this.dashedColor;
}
}
removeDashed(){
if(this.currentEl){
this.currentEl.style.outline = '';
}
}
// 判断是否出现标线
isShowLine(){
const arr = ['bottom','left','top','right'];
const rect = this.getClientRect(this.currentEl);
for(let i = 0; i < this.itemList.length; i++){
const item = this.itemList[i];
const itemInfo = this.getClientRect(item);
if(this.currentEl === item) continue;
for(let j = 0; j < arr.length; j++){
for(let k = 0; k < arr.length; k++){
if(this.RestrictionDirection(arr[k],arr[j]) && rect[arr[k]] == itemInfo[arr[j]]){
this.createShowLine(arr[j],itemInfo[arr[j]]);
}
}
}
}
}
// 限制方向
RestrictionDirection(type1,type2){
const arr = [['bottom','top'],['left','right']];
for(let i = 0; i < arr.length; i++){
if(arr[i].includes(type1) && arr[i].includes(type2)){
return true;
}else if(arr[i].includes(type2) && arr[i].includes(type1)){
return true;
}
}
}
createShowLine(type,num){
// type 方向 num 位置
const arr1 = ['top','bottom'];
const arr2 = ['left','right'];
const line = document.createElement('div');
line.id = 'drag-line';
line.style.cssText = `
position: absolute;
width: ${arr1.includes(type) ? '100%' : '1px'};
height: ${arr2.includes(type) ? '100%' : '1px'};
background-color: ${this.markingColor};
${arr1.includes(type) ? 'top' : 'left'}: ${num}px;
}`;
this.el.appendChild(line);
}
removeShowLine(){
const list = this.$All('#drag-line');
if(list.length === 0) return;
list.forEach((item) => {
item.remove();
})
}
initItemEl(){
const boxItme = 8;
for(let i = 0; i < this.itemList.length; i++){
const item = this.itemList[i];
item.style.position = 'absolute';
item.style.left = '50px';
item.style.top = `${i * 120}px`
for(let j = 0; j < boxItme; j++){
this.addItemBoxDom(item,j);
}
}
}
initItemBox(e){
this.isOutBoundary();
const data_index = Number(this.currentElItem.getAttribute('data-index'));
const id = '.drag-item-box';
const list = this.currentEl.querySelectorAll(id);
const arr = [0,2,5,7];
const arr1 = [1,6];
const {x:initX,y:initY,rectW,rectH,left,top} = this.elInitPointer;
let x = 0;
let y = 0;
let moveX = 0;
let moveY = 0;
if(this.isActive){
const obj = {
0:()=>{
x = left + rectW;
y = top + rectH;
moveX = left;
moveY = top;
},
2:() => {
x = left;
y = top + rectH;
moveX = left + rectW;
moveY = top;
},
5:() => {
x = left + rectW;
y = top;
moveX = left;
moveY = top + rectH;
},
7:() => {
x = left;
y = top;
moveX = left + rectW;
moveY = top + rectH;
},
1:() => {
y = top + rectH;
moveY = top;
},
6:()=>{
y = top;
moveY = top + rectH;
},
3:()=>{
x = left + rectW;
moveX = left;
},
4:()=>{
x = left;
moveX = left + rectW;
},
}
obj[data_index]();
this.elInitPointer.x = x;
this.elInitPointer.y = y;
this.isActive = false;
}else{
x = initX;
y = initY;
moveX = e.clientX;
moveY = e.clientY;
}
// 计算移动后对应元素宽高
const width = Math.abs(x - moveX);
const height = Math.abs(y - moveY);
if(arr.includes(data_index)){
if(moveX > x && moveY > y){ // 4
this.currentEl.style.left = x + 'px';
this.currentEl.style.top = y + 'px';
}else if(y > moveY && x < moveX){ // 1
this.currentEl.style.top = moveY + 'px';
this.currentEl.style.left = x + 'px';
}else if(moveX < x && moveY > y){ // 3
this.currentEl.style.left = moveX + 'px';
this.currentEl.style.top = y + 'px';
}else if(moveY < y && moveX < x){ // 2
this.currentEl.style.top = moveY + 'px';
this.currentEl.style.left = moveX + 'px';
}
this.currentEl.style.height = height + 'px';
this.currentEl.style.width = width + 'px';
}else{
if(arr1.includes(data_index)){
if(moveY > y){
this.currentEl.style.top = y + 'px';
}else{
this.currentEl.style.top = moveY + 'px';
}
this.currentEl.style.height = height + 'px';
}else{
if(moveX > x){
this.currentEl.style.left = x + 'px';
}else{
this.currentEl.style.left = moveX + 'px';
}
this.currentEl.style.width = width + 'px';
}
}
}
addItemBoxDom(dom,index){
/**
* 0 1 2
* 3 4
* 5 6 7
*/
const cursors = ['nw-resize','n-resize','sw-resize','e-resize','w-resize','ne-resize','n-resize','nw-resize']
const div = document.createElement('div');
div.classList = "drag-item-box";
div.setAttribute('data-index',index);
div.style.cssText = `z-index:1000;cursor:${cursors[index]};position: absolute;width:${this.itemBoxSize}px;height:${this.itemBoxSize}px;border:1px solid #000;`;
const rect = this.getClientRect(dom);
let x = 0;
let y = 0;
// itemBoxSize 一半
const itemBoxh = this.itemBoxSize / 2;
const style = {
0:() => {
x = `calc(-${itemBoxh}px)`
y = `calc(-${itemBoxh}px)`;
},
1:()=> {
x = `calc(50% - ${itemBoxh}px)`;
y = `calc(-${itemBoxh}px)`;
},
2:()=> {
x = `calc(100% - ${itemBoxh}px)`;
y = `calc(-${itemBoxh}px)`;
},
3:()=> {
x = `calc(-${itemBoxh}px)`;
y = `calc(50% - ${itemBoxh}px)`;
},
4:()=> {
x = `calc(100% - ${itemBoxh}px)`;
y = `calc(50% - ${itemBoxh}px)`;
},
5:()=> {
x = `calc(-${itemBoxh}px)`;
y = `calc(100% - ${itemBoxh}px)`;
},
6:()=> {
x = `calc(50% - ${itemBoxh}px)`;
y = `calc(100% - ${itemBoxh}px)`;
},
7:()=> {
x = `calc(100% - ${itemBoxh}px)`;
y = `calc(100% - ${itemBoxh}px)`;
}
}
style[index]();
div.style.left = x;
div.style.top = y;
dom.appendChild(div);
}
// 判断方块是否到边界
isOutBoundary(){
const container = this.getClientRect(this.el)
const rect = this.getClientRect(this.currentEl);
if(rect.left <= 0){this.currentEl.style.left = container.left;}
if(rect.top <= 0){this.currentEl.style.top = container.top;}
if(rect.left >= container.width - rect.width){this.currentEl.style.left = container.width - rect.width + 'px';}
if(rect.top >= container.height - rect.height){this.currentEl.style.top = container.height - rect.height + 'px';}
}
// 获取元素属性
getClientRect(dom){
// bottom height left right top width x y
const style = getComputedStyle(dom);
return Object.assign(dom.getBoundingClientRect(),{zIndex:parseInt(style.zIndex)}) ;
}
$(el){
return document.querySelector(el);
}
$All(el){
const el_list = document.querySelectorAll(el);
if(el_list.length <= 0){
return [];
}
return el_list;
}
// 获取子元素数据
getJSONData(file = false){
const data = {};
for(let i = 0; i < this.itemList.length; i++){
const item = this.itemList[i];
const itemInfo = this.getClientRect(item);
data[i] = {
left:itemInfo.left,
top:itemInfo.top,
width:itemInfo.width,
height:itemInfo.height,
z_index:itemInfo.zIndex
}
}
if(file){
const str = JSON.stringify(data);
var blob = new Blob([str], { type: 'text/plain' });
var a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = 'data.txt';
a.click();
}
return data;
}
}
const drag = new Drag({
el:'.container', // 盒子
item:'.drag-item', // 小盒子
marking:true, // 是否显示标线
markingColor:'#000', // 标线颜色
dashed:true, // 是否显示虚线
dashedColor:'#fff',
});
function getData(){
const res = drag.getJSONData();
console.dir(res)
}
</script>
</body>
</html>