quiz icon indicating copy to clipboard operation
quiz copied to clipboard

DOM基础测试32

Open zhangxinxu opened this issue 5 years ago • 21 comments

本期题目如下:

大家提交回答的时候,注意缩进距离,起始位置从左边缘开始;另外,github自带代码高亮,所以请使用下面示意的格式。

```js
// 你的JS代码写在这里
 ```

另外,务必提供在线demo,方便验证与评分。jsbin.com jsfiddle.net codepen.io或者国内类似站点都可以,也可以使用github page。

没有可访问demo积分-1分哟。

-----其他-----

本期小测答疑直播6月1日周六上午10:00,预计半小时左右。直播地址:https://live.bilibili.com/21193211

首位答题者会额外2积分。

感谢您的参与!

zhangxinxu avatar May 29 '19 10:05 zhangxinxu

> 在线 DEMO <

  • 增加了一个矩形范围选框,用以在视觉上直观感受到框选的范围;
  • 兼容移动端
  • 增加了一个按住 box 后“抖动”的效果(感谢袁隆平和他的杂交水稻……)

移动端长按网页会弹出默认菜单,这个菜单会打断长按滑动操作,被这个问题折磨了好久,后来终于找到根因解决了。避坑指南

let boxNums = 20;
new Box(boxNums);  // 调用
class Box {
  constructor(nums = 0) {
    this.nums = isNaN(nums) ? 0 : Math.floor(nums);  // 数量
    this.boxes = [];  // 当前实例的 .box 元素集合
    this.rangeFrame = {  // 范围选框
      el: null,
      x: 0,
      y: 0
    };

    if (nums > 0) {
      this.build();
    }
  }

  // 在 body 元素里创建 .box 盒子
  build() {
    // 使用文档片段,减少性能损耗
    let fragment = document.createDocumentFragment();

    for (let i = 0; i < this.nums; i++) {
      let box = document.createElement('div');

      box.className = 'box';

      // 阻止移动端长按时弹出系统默认菜单
      box.addEventListener('touchstart', e => e.preventDefault());

      this.boxes.push(box);
      fragment.appendChild(box);
    }

    document.body.appendChild(fragment);
    this.bindEvt();
  }

  // 绑定操作事件
  bindEvt() {
    let timer = null;
    let isReady = false;

    // 按下时
    const startHandle = e => {
      e.stopPropagation();

      let elm = e.target;
      
      // “按压”对象为当前实例中创建的 .box 盒子之一
      if (!!~this.boxes.indexOf(e.target)) {
        timer = setTimeout(() => {
          let { clientX, clientY } = e;

          if (e.touches) {  // 移动端
            clientX = e.touches[0].clientX;
            clientY = e.touches[0].clientY;
          }

          isReady = true;
          elm.classList.add('active');
          this.buildRangeFrame(clientX, clientY);
        }, 350);
      } else {  // 点击空白处
        this.boxes.map(box => box.classList.remove('active'));
      }
    };

    // 拖动中
    const moveHandle = e => {
      if (isReady) {
        let { clientX, clientY } = e;

        if (e.touches) {  // 兼容移动端
          clientX = e.touches[0].clientX;
          clientY = e.touches[0].clientY;
        }

        this.updateRangeFrame(clientX, clientY);
      }
    };

    // 松起时,清空定时器(中断高亮操作)
    const endHandle = (e) => {
      e.preventDefault();
      isReady = false;
      clearTimeout(timer);
      this.removeRangeFrame();
    };

    // 按下
    window.addEventListener('touchstart', startHandle);
    window.addEventListener('mousedown', startHandle);

    // 拖动
    window.addEventListener('touchmove', moveHandle);
    window.addEventListener('mousemove', moveHandle);
    
    // 松起
    window.addEventListener('touchend', endHandle);
    window.addEventListener('mouseup', endHandle);
  }

  // 创建范围选框
  buildRangeFrame(posX, posY) {
    let elm = document.createElement('i');

    elm.className = 'range-frame';
    elm.style.left = `${posX}px`;
    elm.style.top = `${posY}px`;
    document.body.appendChild(elm);

    this.rangeFrame.el = elm;
    this.rangeFrame.x = posX;
    this.rangeFrame.y = posY;
  }

  // 更新范围选框
  updateRangeFrame(posX, posY) {
    let { el, x, y } = this.rangeFrame;

    if (posX < x) {  // 向左反方向
      el.style.left = 'auto';
      el.style.right = `${window.innerWidth - x}px`;
    } else {
      el.style.left = `${x}px`;
      el.style.right = 'auto';
    }

    if (posY < y) {  // 向上反方向
      el.style.top = 'auto';
      el.style.bottom = `${window.innerHeight - y}px`;
    } else {
      el.style.top = `${y}px`;
      el.style.bottom = 'auto';
    }

    // 矩形选框尺寸
    el.style.width = `${Math.abs(posX - x)}px`;
    el.style.height = `${Math.abs(posY - y)}px`;

    // 获取矩形区域左上、右下坐标
    // this.computeContains({
    // 	x1: Math.min(posX, x), y1: Math.min(posY, y),
    // 	x2: Math.max(posX, x), y2: Math.max(posY, y)
    // });

    this.computeContains(el.getBoundingClientRect());
  }

  // 移除范围选框
  removeRangeFrame() {
    if (this.rangeFrame.el) {
      document.body.removeChild(this.rangeFrame.el);
      this.rangeFrame.el = null;
    }
  }

  // 计算 box 是否包含在选框区域
  computeContains(area) {
    this.boxes.map(box => {
      let { left, top, width, height } = box.getBoundingClientRect();
      
      // 矩形碰撞检测
      if (
        area.left + area.width > left && left + width > area.left  // 横向
        &&
        area.top + area.height > top && top + height > area.top  // 纵向
      ) {
        box.classList.add('active');
      }
    });
  }
}

分享自己画的矩形碰撞检测示意图: image

// zxx: 框选有bug,一旦框选经过,框选范围再变小的时候,选中态没有还原。
// wingmeng: 谢谢张老师指点,我题意理解错了……

wingmeng avatar May 29 '19 12:05 wingmeng

在线demo

var point = new Proxy({ judgeInPos },{
  set(obj, prop, event) {//记录框选坐标
   e = event.changedTouches || event.touches || event;
   switch (prop) {
    case "x1":case "x2":obj[prop] = e.pageX !== undefined ? e.pageX : e[0].pageX;break;
    case "y1":case "y2":obj[prop] = e.pageY !== undefined ? e.pageY : e[0].pageY;break;
   }
}});
function judgeInPos(it, i) {//判断块包含
 var bi = $scope.box[i],
  bo = {x1: Math.min(point.x1, point.x2),x2: Math.max(point.x1, point.x2),
        y1: Math.min(point.y1, point.y2),y2: Math.max(point.y1, point.y2)};
 return bi.x2 > bo.x1 && bi.x1 < bo.x2 && bi.y2 > bo.y1 && bi.y1 < bo.y2;
}
var $scope = {
 ani: 0,
 box: [],
 normal: fn =>[{ start: "mousedown", move: "mousemove", end: "mouseup" },
               { start: "touchstart",move: "touchmove", end: "touchend" }].forEach(fn),
 noise: fn => ["dragstart", "contextmenu"].forEach(fn),
 on:  document.addEventListener.bind(document),
 off: document.removeEventListener.bind(document),
 isBox: node => node.classList.contains("box"),
 walk:  fn => document.querySelectorAll(".box").forEach(fn),
 paint1: () =>$scope.walk((box, i) =>
              box.classList[judgeInPos(box, i) ? "add" : "remove"]("active")),
 paint2: () =>$scope.walk((box, i) => 
              judgeInPos(box, i) && box.classList.add("active")),
 paint: () => circle.checked ? $scope.paint2() : $scope.paint1(),
 create() {//创建box
  var frag = document.createDocumentFragment(),div,i = 3e4;
  while (i--) {
   div = document.createElement("div");
   div.className = "box";
   frag.appendChild(div);
  }
  document.body.appendChild(frag);
 },
 handleSelect() {//触发框选事件
  $scope.paint();
  $scope.normal(evt => $scope.on(evt.move, $scope.handleMove));
 },
 handleMove(event) {
  //记录位置+渲染box
  point.x2 = point.y2 = event;
  cancelAnimationFrame($scope.ani);
  $scope.ani = window.requestAnimationFrame($scope.paint);
 },
 handleResize() {
  $scope.normal(evt => $scope.off(evt.move, $scope.handleMove));
  $scope.walk(box => box.classList.remove("active"));
 },
 getRect() {
  $scope.walk((box, i) =>($scope.box[i] = {
    x1: box.offsetLeft,y1: box.offsetTop,
    x2: box.offsetLeft + box.offsetWidth,
    y2: box.offsetTop + box.offsetHeight
  }));
 }
};
//清除干扰事件
$scope.noise(evt =>$scope.on(evt, e => $scope.isBox(e.target) && e.preventDefault()));
$scope.create();
//开始监听
$scope.normal(evt => {
 $scope.on(evt.start, event => {
  if ($scope.isBox(event.target)) {
   var isActive = true;
   point.x1 = point.x2 = point.y1 = point.y2 = event;
   $scope.on(evt.end, () => {
    isActive = null;
    $scope.off(evt.move, $scope.handleMove);
    window.removeEventListener("resize", $scope.handleResize);
   });
   $scope.getRect();
   window.addEventListener("resize", $scope.handleResize);
   setTimeout(() => isActive && $scope.handleSelect(), 350);
  } else {
   $scope.walk(box => box.classList.remove("active"));
  }
 });
});

Seasonley avatar May 29 '19 13:05 Seasonley

demo

/**...**/
.box:hover:active{
  opacity:.99;
  transition:opacity 1s;/**通过css过渡设置延时**/
}
var fragment = document.createDocumentFragment();
for (var i = 0; i < 20; i++) {
    var box = document.createElement('div');
    box.className = 'box';
    box.draggable = false;
    fragment.appendChild(box)
}
document.body.appendChild(fragment);

document.addEventListener('transitionend', longclick);

document.addEventListener('mouseup', handleup);

document.addEventListener('touchend', handleup);

var start = [];

function longclick(ev) {
    if (ev.target.className.includes('box')) {
        ev.target.classList.add('active');
        document.addEventListener('mousemove', handlemove);
        document.addEventListener('touchmove', handlemove);
    }
}

function handleup(ev) {
    if (start.length) {
        start.length = 0;
    } else {
        if (!ev.target.className.includes('box')) {
            clear();
        }
    }
    window.getSelection().removeAllRanges();//清除选中区域
    document.removeEventListener('mousemove', handlemove);
    document.removeEventListener('touchmove', handlemove);
}

function handlemove(ev) {
    var ev = ev.touches&&ev.touches[0] || ev;
    if (!start.length) {
        start = [ev.pageX, ev.pageY];
    }
    document.querySelectorAll('.box').forEach(function (el) {
        var _x = Math.min(start[0], ev.pageX);
        var _y = Math.min(start[1], ev.pageY);
        var _l = Math.max(start[0], ev.pageX);
        var _t = Math.max(start[1], ev.pageY);
        var $w = el.offsetWidth;
        var $h = el.offsetHeight;
        var $x = el.offsetLeft;
        var $y = el.offsetTop;
        var $l = $x + $w;
        var $t = $y + $h;
        if ($l - $w < _l && $l > _x && $t - $h <= _t && $t >= _y) {
            el.classList.add('active');
        } else {
            el.classList.remove('active');
        }
    })
}

function clear() {
    document.querySelectorAll('.box').forEach(function (el) {
        el.classList.remove('active');
    })
}

貌似IE有个msElements​From​Rect,可以直接用来判断位置的,可惜非标准

object.msElementsFromRect(left, top, width, height, retVal)

XboxYan avatar May 29 '19 13:05 XboxYan

说好的交卷以后就不修改了,但昨天晚上回家以后再三考虑还缺着些东西,辗转反侧,还是加上了一个辅助框子,真香/(ㄒoㄒ)/~~ 我不知道为啥,我这个辅助框子在jsbin中不能展示出来,在本地执行的时候就能出来,我本来打算放到jsfiddler上面的,但jsfiddler打不开

--最后一次修改日期为: 2019/5/30 上午10:07:44

jsbin

let t,//循环判断按下时长并给与元素激活的句柄
    isActive,//当前元素是否已经被激活了
    startX,//开始框选的起始x
    startY,//开始框选的起始y
    auxiliaryLine;//辅助线
generageBox();
function generageBox(len = 20) {
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < len; i++) {
    const box = document.createElement('div');
    box.classList.add('box');
    box.addEventListener('mouseleave',function(){
      clearInterval(t);
    })
    box.addEventListener('dragstart',function(e){
      e.preventDefault();
    })
    fragment.appendChild(box);
  }
  document.body.appendChild(fragment);
}

document.body.addEventListener('mousedown',function(e){
  const target = e.target.closest('.box');
  if(target){
    const startTime = Date.now();
    t = setInterval(function(){
      if(Date.now() - startTime>=1000){
        target.classList.add('active');
        isActive = true;
        startX = e.pageX,
        startY = e.pageY;
        clearInterval(t);
      }
    });
  }else{
    [].slice.call(document.querySelectorAll('.active'))
    .forEach(function(box){
      box.classList.remove('active')
    });
    isActive = false;
  }
})
document.body.addEventListener('mouseup',function(){
  auxiliaryLine&&document.body.removeChild(auxiliaryLine);
  auxiliaryLine = null;
  isActive=false;
  clearInterval(t);
})
document.body.addEventListener("mousemove",function(e){
  if(isActive){
    const top1 = Math.min(e.pageY,startY),
          right1 = Math.max(e.pageX,startX),
          bottom1 = Math.max(e.pageY,startY),
          left1 = Math.min(e.pageX,startX);
    if(!auxiliaryLine){
      auxiliaryLine = document.createElement('div');
      document.body.appendChild(auxiliaryLine);
    }
    
    Object.assign(auxiliaryLine.style,{
      position:'absolute',
      top:top1,
      left:left1,
      width:Math.abs(startX - e.pageX),
      height:Math.abs(startY - e.pageY),
      border:`1px solid darkgray`,
      background:'rgba(0,0,0,.1)'
    });
    [].slice.call(document.querySelectorAll('.box'))
    .forEach(function(box){
      const top = box.offsetTop,
            left = box.offsetLeft,
            right = left + box.offsetWidth,
            bottom = top + box.offsetHeight;
      box.classList.toggle('active',!(right<left1||bottom<top1||right1<left||bottom1<top));
    })
  }
})
//zxx: document.body绑定还是容易出bug的,因为区域尺寸并非全屏,例如可能点击空格处不会取消选中态。

liyongleihf2006 avatar May 29 '19 13:05 liyongleihf2006

Demo https://codepen.io/crazyboy/pen/vwVxPP

(function() {
	var createDiv = function(className) {
		var ele = document.createElement('div');
		ele.className = className;
		return ele;
	};
	var addEvent = function(ele, type, handler) {
		if (ele.addEventListener) {
			ele.addEventListener(type, handler, false);
		} else if (ele.attachEvent) {
			ele.attachEvent('on' + type, handler);
		} else {
			ele['on' + type] =  handler;
		}
	};
	var isBox = function(ele) {
		return ele.classList.contains('box');
	};
	var isTouch = function() {
		return 'ontouchstart' in document;
	};
	var highlightTrigger = function(interval) {
		var tid, startX, startY, startFlag = false;
		var selectionRect = document.createElement('div');
		selectionRect.style.cssText = 'position:absolute;width:0;height:0;background-color:Highlight;opacity:0.5;';
		document.body.appendChild(selectionRect);
		var isCrossed = function(sStart, sLength, rStart, rLength) {
			return (sStart < rStart && (sStart + sLength) >= rStart) || (sStart >= rStart && sStart <= (rStart + rLength));
		};
		var getEventObj = function(event) {
			if (isTouch()) {
				return event.touches[0];
			} else {
				return event;
			}
		}
		return {
			start: function(event) {
				if (isBox(event.target)) {
					if (event.cancelable && event.preventDefault) {
						event.preventDefault();	
					}
					tid = setTimeout(function () {
						event.target.classList.add('active');
						var eventObj = getEventObj(event);
						startX = eventObj.clientX;
						startY = eventObj.clientY;
						selectionRect.style.left = startX + 'px';
						selectionRect.style.top = startY + 'px';
						startFlag = true;
					}, interval);
				} else {
					[].forEach.call(document.querySelectorAll('.box'), function(ele) {
						ele.classList.remove('active');
					});
				}
			},
			stop: function(event) {
				if (startFlag) {
					setTimeout(function () {
						selectionRect.style.width = 0;
						selectionRect.style.height = 0;
						startFlag = false;
					}, 100);
				} else if (isBox(event.target) && tid) {
					clearTimeout(tid);
				}
			},
			move: function(event) {
				if (startFlag) {
					var eventObj = getEventObj(event);
					selectionRect.style.left = Math.min(startX, eventObj.clientX) + 'px';
					selectionRect.style.top = Math.min(startY, eventObj.clientY) + 'px';
					selectionRect.style.width = Math.abs(eventObj.clientX - startX) + 'px';
					selectionRect.style.height = Math.abs(eventObj.clientY - startY) + 'px';
					[].forEach.call(document.querySelectorAll('.box'), function(ele) {
						if (isCrossed(selectionRect.offsetLeft, selectionRect.offsetWidth, ele.offsetLeft, ele.offsetWidth) && isCrossed(selectionRect.offsetTop, selectionRect.offsetHeight, ele.offsetTop, ele.offsetHeight)) {
							ele.classList.add('active');
						} else {
							ele.classList.remove('active');
						}
					});
				} else if (!isBox(event.target) && tid) {
					clearTimeout(tid);
				}
			}
		}
	}
	for (var i = 0; i < 20; i++) {
		document.body.appendChild(createDiv('box'));
	}
	var myTrigger = highlightTrigger(350);
	addEvent(document.body, 'mousedown', myTrigger.start);
	addEvent(document.body, 'mouseup', myTrigger.stop);
	addEvent(document.body, 'mousemove', myTrigger.move);
	addEvent(document.body, 'mouseleave', myTrigger.stop);

	addEvent(document.body, 'touchstart', myTrigger.start);
	addEvent(document.body, 'touchend', myTrigger.stop);
	addEvent(document.body, 'touchmove', myTrigger.move);
})();
//zxx: 事件绑定在document.body会有事件响应区域问题,局部而非整个页面

NeilChen4698 avatar May 30 '19 04:05 NeilChen4698

demo update: document.body.addEventListener 换成window.addEvenListener能解决移动过快鼠标移出浏览器外之后无法精确获取鼠标位置的bug,原理未知求解答

 <div class="select-area"></div>
    html, body {
        width: 100%;
        height: 100%;
        margin: 0;
        position: relative;
    }
    .box {
        display: inline-block;
        width: 100px;
        height: 100px;
        margin: 10px;
        background-color: gray;
    }
    .box.active {
        background-color: skyblue;
    }
    .select-area {
        position: absolute;
        background-color: rgba(0,0,0,0.2);
    }
    let fragment = document.createDocumentFragment();
    let touchable = "ontouchstart" in window;
    for(let i=0;i<20;i++) {
        let dom = document.createElement('div');
        dom.className = 'box';
        fragment.appendChild(dom);
    }
    document.body.append(fragment);

    let timer;
    let startX, startY;
    let boxes = []
    window.addEventListener(touchable ? 'touchstart' : 'mousedown', mousedownHandler);
    function mousedownHandler(e) {
        let target = e.target;
        if (target.className.indexOf('box') >= 0) {
            timer = setTimeout(() => {
                target.classList.add('active');
                startX = touchable ? e.touches[0].pageX : e.pageX;
                startY = touchable ? e.touches[0].pageY : e.pageY;
                window.addEventListener(touchable ? 'touchmove' : 'mousemove', mousemoveHandler);
            }, 350);
        } else {
            document.querySelectorAll('.box').forEach(item => {
                item.classList.remove('active');
            });
        }
    }
    window.addEventListener(touchable ? 'touchend' : 'mouseup', mouseupHandler);
    function mouseupHandler(e) {
        if (timer) {
            clearTimeout(timer);
        }
        window.removeEventListener(touchable ? 'touchmove' : 'mousemove', mousemoveHandler);
        document.querySelector('.select-area').style.cssText = '';
    }
    function mousemoveHandler(e) {
        let endX = touchable ? e.touches[0].pageX : e.pageX;
        let endY = touchable ? e.touches[0].pageY : e.pageY;
        let {x1, y1, x2, y2} = {x1: Math.min(startX, endX), y1: Math.min(startY, endY), x2: Math.max(startX, endX), y2: Math.max(startY, endY)};
        document.querySelector('.select-area').style.cssText = `top: ${y1}px;left: ${x1}px;width: ${x2 - x1}px;height: ${y2 - y1}px;border: 1px solid black;`;
        let inAreaBoxes = boxes.forEach(box => {
            if (isInArea({x1, y1, x2, y2}, box)) {
                box.classList.add('active');
            } else {
                box.classList.remove('active');
            }
        });
    }
    document.querySelectorAll('.box').forEach(item => {
        let clientRect = item.getBoundingClientRect();
        item.x1 = clientRect.left + document.documentElement.scrollLeft;
        item.y1 = clientRect.top + document.documentElement.scrollTop;
        item.x2 = item.x1 + clientRect.width;
        item.y2 = item.y1 + clientRect.height;
        boxes.push(item)
    });
    function isInArea(area, dom) {
        let {x1, y1, x2, y2} = area;
        if (x2 < dom.x1 || y2 < dom.y1 || x1 > dom.x2 || y1 > dom.y2) {
            return false;
        }
        return true;
    }
    document.body.addEventListener('contextmenu', function(e) {
        e.preventDefault();
    });
//zxx: 碰撞检测应该有问题,会出现框选选中不准确的问题
//me: 碰撞检测有问题是因为在页面resize的时候没有重新获取盒子的位置,使用了一开始的位置
//me: 还有一个问题是第一次按下去之后拖动没到时间不应该触发框选功能

frankyeyq avatar May 30 '19 06:05 frankyeyq

codepan Demo

window.onload = () => {
  // 移除类
  function removeClass(elm, className) {
    elm.classList &&
      elm.classList.contains(className) &&
      elm.classList.remove(className)
  }
  // 添加类
  function addClass(elm, className) {
    elm.classList &&
      !elm.classList.contains(className) &&
      elm.classList.add(className)
  }
  // 是否一直在按着
  let globalTouch = false
  const isMobile = document.ontouchstart !== undefined

  Array(20)
    .fill('')
    .map(v => {
      const box = document.createElement('DIV')
      box.className = 'box'
      const boxAction = new touchAction(box)
      document.body.append(box)
    })

  const boxAction = new touchAction(document.body)
  document.body.addEventListener(
    isMobile ? 'touchstart' : 'mousedown',
    boxAction.startTouch,
  )
  document.body.addEventListener(
    isMobile ? 'touchmove' : 'mousemove',
    boxAction.overTouch,
  )
  document.body.addEventListener(
    isMobile ? 'touchend' : 'mouseup',
    boxAction.endTouch,
  )

  // 设置事件
  function touchAction(elm) {
    var touchTimer
    var startPoint = { x: null, y: null }
    return {
      startTouch: e => {
        startPoint = {
          x: e.pageX || e.targetTouches[0].pageX,
          y: e.pageY || e.targetTouches[0].pageY,
        }
        console.log(e.target.classList.contains('box'))
        if (e.target.classList.contains('box')) {
          touchTimer = setTimeout(() => {
            addClass(e.target, 'active')
            globalTouch = true
          }, 350)
        } else {
          ;[].slice
            .call(document.querySelectorAll('.box'))
            .map(element => {
              removeClass(element, 'active')
            })
        }
      },
      endTouch: e => {
        if (touchTimer) {
          clearTimeout(touchTimer)
          touchTimer = 0
        }
        globalTouch = false
        startPoint = { x: null, y: null }
      },
      overTouch: e => {
        if (globalTouch) {
          ;[].slice
            .call(document.querySelectorAll('.box'))
            .map(function(element, index) {
              isInArea(element, startPoint, {
                x: e.pageX || e.targetTouches[0].pageX,
                y: e.pageY || e.targetTouches[0].pageY,
              })
            })
        }
      },
    }
  }
  function isInArea(elm, start, end) {
    const startPoint = {
      x: Math.min(start.x, end.x),
      y: Math.min(start.y, end.y),
    }
    const endPoint = {
      x: Math.max(start.x, end.x),
      y: Math.max(start.y, end.y),
    }
    const xStart = elm.offsetLeft
    const xEnd = elm.offsetLeft + elm.clientWidth
    const yStart = elm.offsetTop
    const yEnd = elm.offsetTop + elm.clientHeight
    if (
      xEnd <= startPoint.x ||
      yEnd <= startPoint.y ||
      xStart >= endPoint.x ||
      yStart >= endPoint.y
    ) {
      removeClass(elm, 'active')
    } else {
      addClass(elm, 'active')
    }
  }
}

livetune avatar May 30 '19 06:05 livetune

在线预览

    <p id="choose"></p>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .box {
            display: inline-block;
            width: 100px;
            height: 100px;
            margin: 10px;
            background-color: gray;
        }

        .box.active {
            background-color: skyblue;
        }

        #choose {
            display: block;
            position: absolute;
            left: 0px;
            top: 0px;
        }
(function () {

        let num = 20,
            timer = 1,
            longClickTime = 350,
            isActive = false,
            startX,
            startY,
            nowX,
            nowY;

        let aBox = document.getElementsByClassName("box"),
            oP = document.getElementById("choose");

        for (let i = 0; i < num; i++) {
            let oDiv = document.createElement("div");
            oDiv.className = "box";
            document.body.appendChild(oDiv);
        }

        function startHandle(e) {
            e.preventDefault();
            e.stopPropagation();
            let target = e.target;
            if (target.nodeName.toUpperCase() === "DIV") {
                timer = setTimeout(function () {
                    startX = e.pageX;
                    startY = e.pageY;
                    isActive = true;
                    target.classList.add("active");
                }, longClickTime)
            } else {
                [...aBox].forEach((item) => {
                    item.classList.remove('active');
                });
                isActive = false;
            }
        }

        function moveHandle(e) {
            e.preventDefault();
            e.stopPropagation();
            if (isActive) {
                nowX = e.pageX;
                nowY = e.pageY;
                let left = Math.min(startX, nowX);
                let top = Math.min(startY, nowY);
                let width = Math.abs(startX - nowX);
                let height = Math.abs(startY - nowY);
                oP.style.display = "block";
                oP.style.left = left + "px";
                oP.style.top = top + "px";
                oP.style.width = width + "px";
                oP.style.height = height + "px";
                oP.style.backgroundColor = "rgba(0,0,0,.2)";
            }
        }

        function endHandle(e) {
            e.preventDefault();
            e.stopPropagation();
            [...aBox].forEach((item) => {
                let flag = isCollision(item, oP);
                if (flag) {
                    item.classList.add('active');
                }
            });
            oP.style.display = "none";
            isActive = false;
            clearTimeout(timer);
        }

        function isCollision(a, b) {
            var l1 = a.offsetLeft;
            var r1 = a.offsetLeft + a.offsetWidth;
            var t1 = a.offsetTop;
            var b1 = a.offsetTop + a.offsetHeight;

            var l2 = b.offsetLeft;
            var r2 = b.offsetLeft + b.offsetWidth;
            var t2 = b.offsetTop;
            var b2 = b.offsetTop + b.offsetHeight;
            
            if (r1 < l2 || l1 > r2 || b1 < t2 || t1 > b2) {
                return false;
            } else {
                return true;
            }
        }

        document.addEventListener("mousedown", startHandle);
        document.addEventListener("mousemove", moveHandle);
        document.addEventListener("mouseup", endHandle);

    })()
//zxx: 框选效果有问题

guqianfeng avatar May 30 '19 06:05 guqianfeng

demo

// 添加类
function addClass(el,c){
  el.classList.add(c)
}
// 删除类
function removeClass(el,c){
  el.classList.remove(c)
}
// 添加盒子
function addBox(num) {
  var frag = document.createDocumentFragment()
  for(let i=0;i<num;i++) {
    var div = document.createElement('div')
    addClass(div,'box')
    div.draggable = false
    frag.appendChild(div)
  }
  document.body.appendChild(frag)
}

// 添加 20 个
addBox(20)

// 通过事件冒泡委托处理
document.addEventListener('mousedown',handleMouseDown)
document.addEventListener('mouseup',handleMouseUp)


const timeout = 350
var timer = null
// 是否移动
var isMove = false

var startPos = []
var endPos = []

// 鼠标按下
function handleMouseDown(e) {
    
    // 鼠标按下的坐标
    startPos = [e.pageX, e.pageY] 
    // var tarRect = e.target.getBoundingClientRect()
    if(e.target.classList.contains('box')) {
        // 设置计时器 指定时间之后表示box被激活
        timer = setTimeout(function() {
            addClass(e.target,'active')
            clearTimeout(timer)
            timer = null
            document.addEventListener('mousemove',handleMouseMove)
        },timeout)
    }
}
// 鼠标抬起
function handleMouseUp(e) {
    if(!isMove&&!e.target.className.includes('active')) {
        // 点击了没有激活的元素 移除所有的active
        var active_list = document.querySelectorAll('.active')
        active_list.forEach((item) => {
            removeClass(item, 'active')
        })
    }
    clear()
}
function clear() {
    clearTimeout(timer)
    timer = null
    isMove = false
    endPos = []
    startPos = []
    document.removeEventListener('mousemove',handleMouseMove)
}
// 处理鼠标移动
var handleMouseMove = debounce(function mm(e) {
    isMove = true
    if(timer !== null) { 
        clearTimeout(timer)
    } else {
        endPos = [e.pageX, e.pageY]
        var box_list = document.querySelectorAll('.box')
        box_list.forEach((el) => {
            if(isSelect(el)) {
                addClass(el,'active')
            } else {
                removeClass(el,'active')
            }
        })
    }
    
})
function debounce(fn, interval = 0) {
    let timeout = null;
    return function () {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            fn.apply(this, arguments);
        }, interval);
    };
}
// 判断是否被选中
function isSelect(el) {
    // 选区
    var top = Math.min(startPos[1],endPos[1])
    var bottom = Math.max(startPos[1],endPos[1])
    var left = Math.min(startPos[0],endPos[0])
    var right = Math.max(startPos[0],endPos[0])
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
    // 需要判断的 el
    var el_rect = el.getBoundingClientRect()
    return !(el_rect.right < left || el_rect.left > right || el_rect.bottom < top || el_rect.top > bottom)
}
//zxx: 长按选中和框选均有问题,会莫名全选。代码没细看,可能与定时器使用以及碰撞检测不准确有关。

ylfeng250 avatar May 30 '19 08:05 ylfeng250

demo

// 1
const box = '<div class="box"></div>'
let boxHtml = ''
for (let i = 0; i < 20; i++) {
  boxHtml += box
}
const boxFragment = document.createRange().createContextualFragment(boxHtml)
document.body.append(boxFragment)

// 2
const TIME = 350
let active = false
let firstTouch = []

document.body.addEventListener('mousedown', function(event) {
  if (event.target.classList.contains('box')) {
    active = true

    setTimeout(function() {
      if (active == true) {
        event.target.classList.add('active')
        // 3
        firstTouch = [event.clientX, event.clientY]
        document.addEventListener('mousemove', mouseMove)
      }
    }, TIME)
  } else {
    document.querySelectorAll('.box').forEach(function(item) {
      item.classList.remove('active')
    })
  }
})

document.addEventListener('mouseup', function(event) {
  active = false
  document.removeEventListener('mousemove', mouseMove)
})

function mouseMove(event) {
  const clientX = event.clientX
  const clientY = event.clientY

  // 将一开始点击的坐标和移动时候获取的坐标组合成一个矩形
  const rectLeft = Math.min(firstTouch[0], clientX)
  const rectTop = Math.min(firstTouch[1], clientY)
  const rectRight = Math.max(firstTouch[0], clientX)
  const rectBottom = Math.max(firstTouch[1], clientY)

  document.querySelectorAll('.box').forEach(function(element) {
    // 经过测试当页面存在滚动时会出错
    // const left = element.offsetLeft
    // const right = left + element.offsetWidth
    // const top = element.offsetTop
    // const bottom = top + element.offsetHeight

    const client = element.getBoundingClientRect()
    const left = client.left
    const right = left + client.width
    const top = client.top
    const bottom = top + client.height

    // 判断 box 的左/右/上/下边框是否处于矩形内部 -> 判断条件出错
    // const leftFlag = rectLeft <= left && left <= rectRight
    // const rightFlag = rectLeft <= right && right <= rectRight
    // const topFlag = rectTop <= top && top <= rectBottom
    // const bottomFlag = rectTop <= bottom && bottom <= rectBottom

    if (
      left < rectRight &&
      right > rectLeft &&
      top <= rectBottom &&
      bottom >= rectTop
    ) {
      element.classList.add('active')
    } else {
      element.classList.remove('active')
    }
  })
}

//zxx: document.body非全屏,还有交互细节有不准确的地方。

Despair-lj avatar May 30 '19 09:05 Despair-lj

demo

// 第一题
let docFrag = document.createDocumentFragment();
for(let i = 0 ; i< 20 ; i++){
  let ele = document.createElement('div','box');
  ele.textContent = i;
  ele.className = 'box';
  docFrag.appendChild(ele)
}
document.body.append(docFrag);

// 第二题 第三题
let timeFlag = 0;
document.body.addEventListener('mousedown',function(e){
  let currentEle = e.target.className;
  if(currentEle === 'box'){
    timeFlag = Date.now();
    document.body.addEventListener('mousemove',handleMove)
  }
  
  if(currentEle !== "box active" && currentEle !== "box"){
    let activeEle = document.querySelectorAll('.active');
    Array.prototype.forEach.call(activeEle,function(item,index){
      item.className="box"
    })
  }
})
document.body.addEventListener('mouseup',function(e){
  let currentEle = e.target.className;
  if(currentEle === 'box'){
    let nowTime = Date.now();
    if(nowTime - timeFlag > 350){
      e.target.className = "box active"
    }else{
      e.target.className = "box"
    }
  }
  document.body.removeEventListener('mousemove',handleMove)
})


function handleMove(e){
  let mouseX = e.clientX;
  let mouseY = e.clientY;
  document.querySelectorAll('.box').forEach(function(item,index){
    let client = item.getBoundingClientRect();
    let posX = client.left;
    let posY = client.top;
    let width = client.width;
    let height = client.height;
    
    if(mouseX > posX && mouseX < posX + width && mouseY > posY && mouseY < posY + height){
      item.className = "box active"
    }
  })
}
//zxx: 框选有问题,长按选中应该实时,非抬起的时候

simplefeel avatar May 30 '19 10:05 simplefeel

在线案例 多了一个选框的效果

const BoxName = 'box'
const Time = 350
//第一题 添加20个box
const appendBox = (num=1) => {
    let fragment = document.createDocumentFragment()
    for (let i = 0; i < num; i++){
        let box = document.createElement('div')
        box.className = BoxName
        fragment.appendChild(box)
    }
    document.body.appendChild(fragment)
}
appendBox(20)

//第二题和第三题
const boxes = document.querySelectorAll('.box')
const isTouch = () => 'ontouchstart' in document.body
//切换box的active
const toggleActiveBox = (el, bool) => {
    if (typeof el.length === 'number'){
        for (let i = 0; i < el.length; i++){
            el[i].classList.toggle('active', bool)
        }
    }else {
        el.classList.toggle('active', bool)
    }
}
//展示框选范围
const showCover = (() => {
    const cover = document.querySelector('.cover')
    
    return (position={
        left: 0,
        top: 0,
        width: 0,
        height: 0
    }, bool) => {
        if (bool === false){
            cover.style.display = 'none'
            return
        }
        cover.style.cssText = `top:${position.top}px;left:${position.left}px;width:${position.width}px;height:${position.height}px;display:block;`
    }
})()
//把在框选范围内的box切换成active
const select = ({top, left, width, height}) => {
    let right = left + width
    let bottom = top + height

    for (let i = 0; i < boxes.length; i++){
        let box = boxes[i]
        if (isInSelect({el: box, top, left, right, bottom})) {
            toggleActiveBox(box, true)
        }
    }
}
//判断box是否在框内
const isInSelect = ({el, top, left, bottom, right}) => {
    let bounding = el.getBoundingClientRect()
    if (bounding.right < left || bounding.bottom < top || bounding.left > right || bounding.top > bottom){
        return false
    }
    return true
}
//事件处理
const bindEvent = () => {
    const body = document.body
    let down = 'mousedown'
    let move = 'mousemove'
    let up = 'mouseup'
    if (isTouch()){
        down = 'touchstart'
        move = 'touchmove'
        up = 'touchend'
    }

    body.addEventListener(down, handleDown, false)
    body.addEventListener(move, handleMove, false)
    body.addEventListener(up, handleUp, false)

    const LongTap = 'LongTap'
    const DragSelect = 'DragSelect'
    const postion = {
        top: 0,
        left: 0,
        width: 0,
        height: 0
    }

    let startTime = 0
    let moveTime = 0
    let currentBox = null
    let eventType = ''

    function handleDown(e){
        e.preventDefault()
        startTime = Date.now()
        if (e.target.classList.contains(BoxName)) {
            currentBox = e.target
            eventType = LongTap
            postion.left = e.clientX
            postion.top = e.clientY
        }
    }

    function handleMove(e){
        if (eventType === '') return
        if (moveTime === 0) moveTime = Date.now()

        if (moveTime - startTime > Time){
            eventType = DragSelect

            postion.width = e.clientX - postion.left
            postion.height = e.clientY - postion.top
            showCover(postion)
            select(postion)
        }
    }

    function handleUp(e){

        if (Date.now() - startTime > Time ){
            if (eventType === LongTap && currentBox === e.target){
                toggleActiveBox(currentBox, true)
            }else if (eventType === DragSelect){
                showCover(postion, false)
            }

        }else if (!e.target.classList.contains(BoxName)){
            toggleActiveBox(boxes, false)
        }

        currentBox = null
        eventType = ''
        moveTime = 0
    }

}

bindEvent()
//zxx: 1. 点击长按高亮实际不对; 2. 点击靠近底部的空白不会取消高亮; 3. 框选有问题,往左无效。

jsweber avatar May 30 '19 12:05 jsweber

在线DEMO

好好学习,天天向上

//=> 这样追加,只能追加一个。在我原来的理解中,创建一个类名为box的div标签,循环追加该标签20次才对,
//=> 实际执行,发现页面中只有一个`div.box`为啥捏?
let length = 20;
let $div = document.createElement('div');
$div.className = 'box';
for (let i = 0; i < length; i += 1) {
  document.body.append($div);
}

//zxx: 因为无论是append还是appendChild本质上都是DOM节点的转移,你需要append cloneNode()才是
//=> 1
let length = 20;
for (let i = 0; i < length; i += 1) {
  let $div = document.createElement('div');
  $div.className = 'box';
  document.body.append($div);
}

//=> 2
let timer = null;
let moveFlag = false;
let allBox = document.getElementsByClassName('box');
function down(e) {
  e = e || window.event;
  let target = e.target;

  //=> 记录起始位置
  let scrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
  let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  this.startX = e.clientX + scrollLeft;
  this.startY = e.clientY + scrollTop;

  if (target.className.indexOf('box') > -1) {
    timer = setTimeout(function() {
      target.className = 'box active';
      document.addEventListener('mousemove', move);
      document.addEventListener('touchmove', move);
      clearTimeout(timer);
    },
    350);
  } else {
    Array.prototype.slice.call(allBox).forEach(item = >{
      item.className = 'box';
    });
  }
  document.addEventListener('mouseup', up);
  document.addEventListener('touchend', up);

}
function up() {
  console.log({
    moveFlag
  });
  timer ? clearTimeout(timer) : null;
  moveFlag ? setSelectBox(this) : null;
  document.removeEventListener('mouseup', up);
  document.removeEventListener('touchend', up);
  document.removeEventListener('mousemove', move);
  document.removeEventListener('touchmove', move);
  resetBoxAttribute(this);
}
document.addEventListener('mousedown', down);
document.addEventListener('touchstart', down);

//=> 3
function move(e) {
  moveFlag = true;
  e = e || window.event;
  let scrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
  let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  this.curX = e.clientX + scrollLeft;
  this.curY = e.clientY + scrollTop;
  this.moveX = this.curX - this.startX;
  this.moveY = this.curY - this.startY;
  let width = Math.abs(this.moveX);
  let height = Math.abs(this.moveY);
  let left = this.moveX >= 0 ? this.startX: (this.startX + this.moveX);
  let top = this.moveY >= 0 ? this.startY: (this.startY + this.moveY);
  setBoxAttribute(left, top, width, height, 1);

}

//=> 阴影选区部分
let selectBox = document.getElementsByClassName('select-box')[0];
function setBoxAttribute(left, top, width, height, opacity) { //=> 设置选区属性
  selectBox.style.left = left + 'px';
  selectBox.style.top = top + 'px';
  selectBox.style.width = width + 'px';
  selectBox.style.height = height + 'px';
  selectBox.style.opacity = opacity;
}

function resetBoxAttribute(_this) { //=> 重置选区以及数据
  selectBox.style.left = 0;
  selectBox.style.top = 0;
  selectBox.style.width = 0;
  selectBox.style.height = 0;
  selectBox.style.opacity = 0;
  moveFlag = false;
  _this.startX = null;
  _this.startY = null;
  _this.curX = null;
  _this.curY = null;
  _this.moveX = null;
  _this.moveY = null;
}

function setSelectBox(_this) { //=> 选区框选
  let startX = _this.startX;
  let endX = _this.curX;
  let startY = _this.startY;
  let endY = _this.curY;
  Array.prototype.slice.call(allBox).forEach(item = >{
    let offsetX = item.offsetLeft;
    let offsetY = item.offsetTop;
    let a,
    b,
    c,
    d,
    e,
    f,
    XSelected,
    YSelected;
    if (startX >= endX) { [startX, endX] = [endX, startX]
    }
    if (startY >= endY) { [startY, endY] = [endY, startY]
    }

    a = startX >= offsetX && startX <= offsetX + 100;
    if (a) {
      XSelected = true;
    } else {
      b = endX >= offsetX && endX <= offsetX + 100;
      if (b) {
        XSelected = true;
      } else {
        c = offsetX >= startX && offsetX + 100 <= endX;
        if (c) {
          XSelected = true;
        }
      }
    }

    d = startY >= offsetY && startY <= offsetY + 100;
    if (d) {
      YSelected = true;
    } else {
      e = endY >= offsetY && endY <= offsetY + 100;
      if (e) {
        YSelected = true;
      } else {
        f = offsetY >= startY && offsetY + 100 <= endY;
        if (f) {
          YSelected = true;
        }
      }
    }

    if (XSelected && YSelected) {
      item.className = 'box active';
    }
  });
  moveFlag = false;
}
//zxx: 框选应实时,一些逻辑实现略啰嗦,命名以及缩进对于阅读不太友好

smileyby avatar May 30 '19 13:05 smileyby

在线demo

.box{display: inline-block;width: 100px;height: 100px;margin: 10px;background-color: gray;}
.box.active{background-color: skyblue;}
.box:active{
    opacity: 0.99;transition: opacity 0.35s;
}
for (var i = 0; i < 20; i++) {
    var box = document.createElement('div');
    box.setAttribute('class','box');
    document.body.appendChild(box);
}
var x1;
var y1;
var x2;
var y2;
var start = false;
document.querySelectorAll('.box').forEach(function (item) {
    item.addEventListener('transitionend',function (event) {
        item.setAttribute('class',item.getAttribute('class')+' active');

        start = true;
    });
    item.onmousedown = function(event){
        x1 = event.clientX;
        y1 = event.clientY;

    };

});
document.onmousedown = function(event){
    if(event.srcElement.className.indexOf('box')==-1){
        document.querySelectorAll('.box').forEach(function (item1){
            item1.setAttribute('class','box');
        });
    }
};
document.onmouseup = function (event) {
    x2 = event.clientX;
    y2 = event.clientY;
    console.log(x1+'---'+x2+'---'+y1+'---'+y2);
    var left = 0;
    var top = 0;
    var width = 100;
    var height = 100;
    if(start) {
        document.querySelectorAll('.box').forEach(function (item) {
            left = item.offsetLeft;
            top = item.offsetTop;
            if (left > (x1-width) && left < x2 && top > y1-height && top < y2) {
                item.setAttribute('class', item.getAttribute('class') + ' active');
            }
        });
    }
    start = false;
}
//zxx: 框选交互问题较多

tzmy avatar May 31 '19 09:05 tzmy

我是一个做不出第三题的demo

// 1.在body元素内append 20个类名是.box的盒子
for(var i = 0 ; i < 20 ;i++){
  let box = document.createElement('div');
  box.className = 'box';
  document.body.appendChild(box);
}

// 2.鼠标或者手指长按盒子元素350秒,新增类名.active; 普通点击无任何变化;点击空白移除所有.active元素类名
function touchstart(e) {
     console.log('touchstart');
     timeOutEvent = setTimeout(function () {
        e.preventDefault();
        console.log(e);
        if(e.target!=document.body){
          // console.log(e.target);
          e.target.classList.add("active");
        }
     }, 350);
 }

function touchmove(e) {
   // console.log('touchmove e',e);
   e.preventDefault();
   console.log('e.clientX', e.clientX);
   console.log('e.clientY', e.clientY);
}

function touchend(e){
   // console.log('touchend');
   clearTimeout(timeOutEvent);
}
// let boxs = document.querySelectorAll("box");

document.body.addEventListener("touchstart", touchstart);
document.body.addEventListener("mousedown", touchstart);

document.body.addEventListener("touchmove", touchmove);
document.body.addEventListener("mousedown", touchmove);

document.body.addEventListener("mouseup", touchend);
document.body.addEventListener("touchend", touchend);


// 3.鼠标或者手指长按盒子元素350秒(不抬起),然后滑动框选,框选范围内所有盒子元素新增类名.active背景高亮


CandyQiu avatar May 31 '19 10:05 CandyQiu

在线DOM

const currEvent = {
    down: 'mousedown',
    move: 'mousemove',
    up: 'mouseup'
};
let down = false,
     isActive = false,
     boxs = [],
     startCX,
     startCY;

const frag = document.createDocumentFragment();
for (let j = 0; j < 20; j++) {
    const box = document.createElement('div');
    box.className = 'box';
    frag.appendChild(box);
}
document.body.appendChild(frag);

document.querySelectorAll('.box').forEach(item = >{
    item.x1 = item.offsetLeft,
    item.x2 = item.x1 + item.offsetWidth,
    item.y1 = item.offsetTop,
    item.y2 = item.y1 + item.offsetHeight;
    boxs.push(item)
});

const handleDwon = e = >{
    const select = e.target.className === 'box'
    if (select) {
        down = true;
        setTimeout(() = >{
            if (down) {
                isActive = true;
                startCX = e.clientX;
                startCY = e.clientY;
                e.target.className = 'box active';
            }
        },
        350)
    } else {
        boxs.forEach(item = >{
            item.className = 'box'
        })
    }
}

const handleMove = e = >{
    if (isActive) {
        const markX1 = Math.min(startCX, e.clientX),
        markX2 = Math.max(startCX, e.clientX),
        markY1 = Math.min(startCY, e.clientY),
        markY2 = Math.max(startCY, e.clientY);
        boxs.forEach(item = >{
            if (item.x1 > markX2 || item.x2 < markX1 || item.y1 > markY2 || item.y2 < markY1) {
                item.className = 'box';
            } else {
                item.className = 'box active';
            }
        })
    }
}
const browser = {
    versions: function() {
        const u = navigator.userAgent,
        app = navigator.appVersion;
        return {
            trident: u.indexOf('Trident') > -1,
            //IE内核
            presto: u.indexOf('Presto') > -1,
            //opera内核
            webKit: u.indexOf('AppleWebKit') > -1,
            //苹果、谷歌内核
            gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1,
            //火狐内核
            mobile: !!u.match(/AppleWebKit.*Mobile.*/),
            //是否为移动终端
            ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),
            //ios终端
            android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1,
            //android终端
            iPhone: u.indexOf('iPhone') > -1,
            //是否为iPhone或者QQHD浏览器
            iPad: u.indexOf('iPad') > -1,
            //是否iPad
            webApp: u.indexOf('Safari') == -1,
            //是否web应该程序,没有头部与底部
            weixin: u.indexOf('MicroMessenger') > -1,
            //是否微信 (2015-01-22新增)
            qq: u.match(/\sQQ/i) == " qq" //是否QQ
        };
    } (),
}

// if (browser.versions.mobile || browser.versions.android || browser.versions.ios) {
if ('ontouchstart' in document.body ) {
    currEvent.down = 'touchstart';
    currEvent.move = 'touchmove';
    currEvent.up = 'touchend';
} else {
    currEvent.down = 'mousedown';
    currEvent.move = 'mousemove';
    currEvent.up = 'mouseup';
}

document.body.addEventListener(currEvent.down, handleDwon);
document.body.addEventListener(currEvent.move, handleMove);
document.body.addEventListener(currEvent.up, e = >{
    down = false;
    isActive = false;
});
//zxx: 点击自身不会取消选中,点击底部空白没有取消选中,框选范围不对,bug很明显,demo页面打开无法执行,=> 全部变成了= >

miaomiaolin avatar May 31 '19 11:05 miaomiaolin

demo

let fragment = document.createDocumentFragment()
for (let i = 0; i < 20; i++) {
  let oDiv = document.createElement('div')
  oDiv.className = 'box'

  fragment.appendChild(oDiv)
}
document.body.appendChild(fragment)

let boxTimer = null
let isOver = false
let initPos = {
  clientX: 0,
  clientY: 0
}
// 鼠标按下||手指长按
document.body.addEventListener('mousedown', handleClick)
document.body.addEventListener('touchstart', handleClick)
function handleClick (e) {
  e.stopPropagation()
  
  if ([...e.target.classList].includes('box')) {
    boxTimer = setTimeout(() => {
      e.target.classList.add('active')
      clearTimeout(boxTimer)
      isOver = true
      initPos = {
        clientX: e.touches && e.touches[0].clientX || e.clientX,
        clientY: e.touches && e.touches[0].clientY || e.clientY 
      }
    }, 350)
  } else {
    let aDiv = [...document.querySelectorAll('.active')]
    aDiv.forEach(item => {
      item.classList.remove('active')
    })
  }
}
// 鼠标或手指抬起
document.body.addEventListener('mouseup', cancelClick)
document.body.addEventListener('touchsend', cancelClick)

function cancelClick (e) {
   isOver = false
    initPos = {
    clientX: 0,
    clientY: 0
  }
   clearTimeout(boxTimer)
}

// 鼠标或手指滑动
document.body.addEventListener('mousemove', handleMove)
document.body.addEventListener('touchmove', handleMove)

function handleMove (e) {
  if (isOver) {
    let minX = Math.min(initPos.clientX, e.touches && e.touches[0].clientX || e.clientX)
    let minY = Math.min(initPos.clientY, e.touches && e.touches[0].clientY || e.clientY)
    let maxX = Math.max(initPos.clientX, e.touches && e.touches[0].clientX || e.clientX)
    let maxY = Math.max(initPos.clientY, e.touches && e.touches[0].clientY || e.clientY)
    
    
    let aDiv = [...document.querySelectorAll('.box')]
    
    aDiv.forEach((item, index) => {
      
      let _width = item.offsetWidth
      let _height = item.offsetHeight
      let clientX = item.offsetLeft
      let clientY = item.offsetTop
      let maxClientX = clientX + _width
      let maxClientY = clientY + _height
      
      if (( maxX > clientX && maxY >= clientY ) && ( maxClientX > minX && maxClientY > minY )) {
        item.classList.add('active')
      } else {
        item.classList.remove('active')
      }
    })
  }
}
//zxx: 框选有问题,点击空白隐藏也有问题

asyncguo avatar May 31 '19 11:05 asyncguo

var timer = null,
    x = 0,
    y = 0,
    isMove = false;

for (var i = 0; i < 20; i++) {
    var box = document.createElement('div');
    box.className = 'box';
    document.body.append(box);
}
//添加方法
function addActive(e){
    if(e.target.className.indexOf('box') >= 0){
        timer = setTimeout(function () {
            e.target.classList.add('active');
            clearTimeout(timer);
            isMove = true;
            x = e.touches && e.touches[0].clientX || e.clientX;
            y = e.touches && e.touches[0].clientY || e.clientY;
        },350);
    }else{
        document.querySelectorAll('.box').forEach(function (item) {
            item.classList.remove('active');
        })
    }
}
//删除方法
function removeActive() {
    isMove = false;
    x = y = 0;
    clearTimeout(timer);
}
//移动方法
function moveActive(e) {
    var ev = e.touches && e.touches[0] || e;
    if (isMove) {
        var left = Math.min(x, ev.clientX);
        var top = Math.min(y, ev.clientY);
        var right = Math.max(x, ev.clientX);
        var bottom = Math.max(y, ev.clientY);
        document.querySelectorAll('.box').forEach((item) => {
            var ow = item.offsetWidth,
                oh = item.offsetHeight,
                ox = item.offsetLeft,
                oy = item.offsetTop;
            var xw = ox + ow;
            var yh = oy + oh;
            if ( right > ox && bottom >= oy  && xw > left && yh > top ) {
                item.classList.add('active')
            } else {
                item.classList.remove('active')
            }
        })
    }
}
//绑定函数
document.body.addEventListener('mousedown',addActive);
document.body.addEventListener('touchstart',addActive);
document.body.addEventListener('mouseup', removeActive);
document.body.addEventListener('touchsend', removeActive);
document.body.addEventListener('mousemove', moveActive);
document.body.addEventListener('touchmove', moveActive);
//zxx: 和上面现象类似

silverWolf818 avatar May 31 '19 12:05 silverWolf818

demo

let box
let fragment = document.createDocumentFragment();
for (var i = 0; i < 20; i++) {
  box = document.createElement("div")
  box.className = "box"
  fragment.appendChild(box)
}
document.body.appendChild(fragment)
let boxs = document.querySelectorAll(".box")
let is_mobi = navigator.userAgent.toLowerCase().match(/(ipod|ipad|iphone|android|coolpad|mmp|smartphone|midp|wap|xoom|symbian|j2me|blackberry|wince)/i) != null;
let startPosx,startPosy,endPosx,endPosy,script,startTime,endTime
let istouch = false
let funarr = []
// 长按加active
boxs.forEach((item, index) => {
  funarr[index] = function(e){
      endTime = new Date().getTime()
      if(endTime-startTime>350){
        e.target.classList.add("active")
        endTime = 0
        startTime = 0
      }
    }
  if (is_mobi) {
    item.addEventListener("touchstart", function(e) {
      startPosx = e.changedTouches[0].clientX
      startPosy = e.changedTouches[0].clientY
      startTime = new Date().getTime()
      boxs.forEach((i,index)=>{
         i.addEventListener("touchmove",funarr[index])
      })
    })
    item.addEventListener("touchend", function(e) {
      endPosx = e.changedTouches[0].clientX
      endPosy = e.changedTouches[0].clientY
      endTime = new Date().getTime()
      boxs.forEach((i,index)=>{
        i.removeEventListener("touchmove",funarr[index])
      })
      if(endTime-startTime>350&&startPosx==endPosx&&startPosy==endPosy){
        this.classList.add("active")
        endTime = 0
        startTime = 0
      }
    })
  } else {
    item.addEventListener("mousedown", function(e) {
      startPosx = e.clientX
      startPosy = e.clientY
      startTime = new Date().getTime()
      boxs.forEach((i,index)=>{
         i.addEventListener("mousemove",funarr[index])
      })
    })
    
    item.addEventListener("mouseup", function(e) {
      endPosx = e.clientX
      endPosy = e.clientY
      endTime = new Date().getTime()
      boxs.forEach((i,index)=>{
        i.removeEventListener("mousemove",funarr[index])
      })
      if(endTime-startTime>350&&startPosx==endPosx&&startPosy==endPosy){
        this.classList.add("active")
        endTime = 0
        startTime = 0
      }

    })
  }
})

// 点击空白处移除active
document.addEventListener("mousedown",function(e){
  if(!e.target.classList.contains("box")){
      boxs.forEach((item,index)=>{
        item.classList.contains("box")&&item.classList.remove("active")
      }) 
  }
})
//zxx: append没问题,实现挺好。第2题选中应该是按下时候执行,而非抬起。

sghweb avatar May 31 '19 17:05 sghweb

  1. 学习的诀窍是什么?最大的诀窍就是坚持。你就能比90%的人优秀。
  2. 直接快速滑动是不应该选中的,在手机端,会和滚动交互冲突。
  3. 移动端长按网页会弹出默认菜单,取消方法:https://codepen.io/wingmeng/pen/PvymKN
  4. wingmeng的碰撞检测没有问题。
  5. createDocumentFragment片段创建提高性能,requestAnimationFrame提高绘制性能,缓存box位置,resize的时候无需重新获取,提高性能。Seasonley是一个比较关注性能的伙伴。
  6. 三人行,必有我师。longclick的检测,我们以前全部都使用setTimeout,XboxYan使用了一个transition检测,配合transitionend回调。这种方法很巧妙,也有一个优点,我们无需clearTimeout这样的操作,浏览器帮我执行了取消检测。也不是完全完美,移动端还需要多多检测下。
  7. 基础API掌握很扎实的来阅文面试我肯定是力推。
  8. 移动和PC可以统一使用方法,不同设备下mousedown/touchstart, mousemove/touchmove, mouseup/touchend,通过判断处理。判断是否是移动设备直接:'ontouchstart' in document.body 。同时document.addEventListener('mouseup', handleup);document.addEventListener('touchend', handleup);这样是有问题的,因为会重复触发handleup。
  9. 碰撞检测比较好理解的算法。A盒子最左边比B最左边小,或A盒子最右边比B最右边大,或上小,下大,再整体取非。

zhangxinxu avatar Jun 01 '19 02:06 zhangxinxu

感谢大佬,这里有一篇文章https://github.com/XboxYan/notes/issues/4,欢迎开脑洞^#^

XboxYan avatar Jun 01 '19 14:06 XboxYan